Linux Trace Kernel
 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; 51+ 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] 51+ 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; 51+ 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] 51+ 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:56   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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:58   ` sashiko-bot
  2026-08-18 14:49 ` [RFC PATCH v1 04/25] unwind_user: Flexible FP/RA recovery rules Jens Remus
                   ` (22 subsequent siblings)
  25 siblings, 1 reply; 51+ 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] 51+ 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:58   ` sashiko-bot
  2026-08-18 14:49 ` [RFC PATCH v1 05/25] unwind_user: Flexible CFA " Jens Remus
                   ` (21 subsequent siblings)
  25 siblings, 1 reply; 51+ 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] 51+ 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:57   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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:57   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:02   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:08   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:05   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:18   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:09   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:10   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:00   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:08   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:11   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:06   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:04   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:13   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:08   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:04   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:10   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:10   ` sashiko-bot
  2026-08-18 14:49 ` [RFC PATCH v1 23/25] s390/ptrace: Provide frame_pointer() Jens Remus
                   ` (3 subsequent siblings)
  25 siblings, 1 reply; 51+ 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] 51+ 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 15:06   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:15   ` sashiko-bot
  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, 1 reply; 51+ 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] 51+ 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 15:17   ` sashiko-bot
  2026-08-18 17:21 ` [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Steven Rostedt
  25 siblings, 1 reply; 51+ 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] 51+ messages in thread

* Re: [RFC PATCH v1 02/25] unwind_user: Stop when reaching an outermost frame
  2026-08-18 14:49 ` [RFC PATCH v1 02/25] unwind_user: Stop when reaching an outermost frame Jens Remus
@ 2026-08-18 14:56   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 14:56 UTC (permalink / raw)
  To: Jens Remus
  Cc: Alexander Gordeev, Vasily Gorbik, linux-s390, Heiko Carstens,
	linux-trace-kernel, Christian Borntraeger

> 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>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=2


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

* Re: [RFC PATCH v1 06/25] unwind_user: Enable archs that define CFA = SP_callsite + offset
  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:57   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 14:57 UTC (permalink / raw)
  To: Jens Remus
  Cc: Alexander Gordeev, Christian Borntraeger, linux-trace-kernel,
	Heiko Carstens, Vasily Gorbik, linux-s390

> 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>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=6


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

* Re: [RFC PATCH v1 05/25] unwind_user: Flexible CFA recovery rules
  2026-08-18 14:49 ` [RFC PATCH v1 05/25] unwind_user: Flexible CFA " Jens Remus
@ 2026-08-18 14:57   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 14:57 UTC (permalink / raw)
  To: Jens Remus
  Cc: Christian Borntraeger, linux-trace-kernel, Alexander Gordeev,
	linux-s390, Heiko Carstens, Vasily Gorbik

> 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>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=5


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

* Re: [RFC PATCH v1 04/25] unwind_user: Flexible FP/RA recovery rules
  2026-08-18 14:49 ` [RFC PATCH v1 04/25] unwind_user: Flexible FP/RA recovery rules Jens Remus
@ 2026-08-18 14:58   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 14:58 UTC (permalink / raw)
  To: Jens Remus
  Cc: linux-s390, Christian Borntraeger, linux-trace-kernel,
	Vasily Gorbik, Heiko Carstens, Alexander Gordeev

> 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>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=4


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

* Re: [RFC PATCH v1 03/25] unwind_user: Enable archs that pass RA in a register
  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:58   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 14:58 UTC (permalink / raw)
  To: Jens Remus
  Cc: Alexander Gordeev, Vasily Gorbik, Heiko Carstens, linux-s390,
	Christian Borntraeger, linux-trace-kernel

> 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>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=3


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

* Re: [RFC PATCH v1 13/25] unwind_user/eh_frame: Show file name in debug output
  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 15:00   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:00 UTC (permalink / raw)
  To: Jens Remus
  Cc: Alexander Gordeev, Christian Borntraeger, Vasily Gorbik,
	linux-trace-kernel, Heiko Carstens, linux-s390

> 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>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=13


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

* Re: [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 07/25] unwind_user/eh_frame: Add support for reading .eh_frame_hdr section Jens Remus
@ 2026-08-18 15:02   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:02 UTC (permalink / raw)
  To: Jens Remus
  Cc: Christian Borntraeger, Alexander Gordeev, Heiko Carstens,
	linux-s390, Vasily Gorbik, linux-trace-kernel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Integer overflow in binary search table size calculation allows bypassing `.eh_frame_hdr` bounds check.
- [Medium] The `read_encoded_pointer` function incorrectly overwrites the computed base address when handling the `DW_EH_PE_absptr` format combined with relative application flags.
--

--- Patch [7]: [RFC PATCH v1 07/25] unwind_user/eh_frame: Add support for reading .eh_frame_hdr section ---
commit 0ca4af16fa2ddbd53ffc639d7ba44b189726700c
Author: Jens Remus <jremus@linux.ibm.com>

unwind_user/eh_frame: Add support for reading .eh_frame_hdr section

This commit adds basic infrastructure for reading the .eh_frame_hdr
section from userspace in preparation for .eh_frame based stack unwinding.

> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> new file mode 100644
> index 0000000000000..8d2b638145bd4
> --- /dev/null
> +++ b/kernel/unwind/eh_frame.c
[ ... ]
> +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;

[Severity: Medium]
Does this overwrite the base address that was just computed for relative
encodings?

When handling DW_EH_PE_absptr in combination with relative application flags
like DW_EH_PE_pcrel, the base address is stored in the result variable above.
However, UNSAFE_GET_USER_INC directly assigns the read value to result
instead of adding it, which will discard the base address.

Should this read into a temporary variable and add it to result, similar to
how the other formats are handled?

> +	case DW_EH_PE_uleb128: {
> +		unsigned long tmp;
> +		ret = read_uleb128(&cur, end, &tmp);
> +		if (ret)
> +			return ret;
> +		result += tmp;
> +		break;
> +	}
[ ... ]
> +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;
[ ... ]
> +		/* 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;

[Severity: Medium]
Can a maliciously large fde_count cause an integer overflow here?

Since fde_count is parsed from the untrusted user eh_frame_hdr section,
the multiplication of fde_count * entry_size could wrap around to a small
positive value.

This would bypass the bounds check against sec->eh_frame_hdr_end and
potentially allow the kernel to trust an arbitrarily large fde_count,
leading to out-of-bounds kernel memory reads when performing binary searches.

Would it be safer to use check_mul_overflow() and check_add_overflow() for
these bounds calculations?

> +	}
> +
> +end:

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=7

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

* Re: [RFC PATCH v1 20/25] unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86
  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 15:04   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:04 UTC (permalink / raw)
  To: Jens Remus
  Cc: Vasily Gorbik, Christian Borntraeger, linux-trace-kernel,
	Heiko Carstens, Alexander Gordeev, linux-s390

> 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>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=20


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

* Re: [RFC PATCH v1 17/25] unwind_user/eh_frame: Ignore DW_CFA_GNU_args_size
  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 15:04   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:04 UTC (permalink / raw)
  To: Jens Remus
  Cc: Heiko Carstens, Vasily Gorbik, linux-s390, Alexander Gordeev,
	Christian Borntraeger, linux-trace-kernel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

Pre-existing issues:
- [High] An integer overflow when evaluating `cur + expr_len` bypasses bounds checks, allowing an attacker to move the instruction pointer backward and trap the kernel in an infinite loop.
- [High] The CFI interpreter loop does not impose a maximum execution limit, allowing an attacker to stall the CPU by providing a maliciously crafted, highly padded sequence of CFI instructions.
--

--- Patch [17]: [RFC PATCH v1 17/25] unwind_user/eh_frame: Ignore DW_CFA_GNU_args_size ---
commit e1c2b0ab16134f39bfe2a1cb074739516466f777
Author: Jens Remus <jremus@linux.ibm.com>

unwind_user/eh_frame: Ignore DW_CFA_GNU_args_size

This commit updates the unwind mechanism to safely ignore the
DW_CFA_GNU_args_size DWARF instruction. This instruction is used by GCC
to track exception handling stack sizes, but is not needed for kernel
stack tracing.

> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> index ac288cec8021f..64176242b7d8f 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;

[Severity: High]
This is a pre-existing issue, but does this code allow an integer overflow
when evaluating cur + expr_len?

kernel/unwind/eh_frame.c:__do_cfi_insn() {
    ...
		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 a maliciously crafted .eh_frame section provides an extremely large
expr_len value, cur + expr_len can wrap around to a value less than end.
This bypasses the bounds check, and when the instruction pointer cur is
updated via cur += expr_len, it moves backwards.

Since the unwinder operates on user-supplied call frame information, can
this trap the kernel in an uninterruptible infinite loop and cause a local
denial of service?


[Severity: High]
This is a pre-existing issue, but is it possible for this loop to run without
an execution limit?

kernel/unwind/eh_frame.c:__do_cfi_program() {
    ...
		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;
		}
    ...
}

If a local attacker maps a maximally sized frame description entry filled
with DW_CFA_nop instructions, the kernel will sequentially process billions
of instructions without yielding or aborting.

Since this can run in non-preemptible or non-maskable interrupt contexts
during stack unwinding, can this stall the processor and trigger a
watchdog panic?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=17

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

* Re: [RFC PATCH v1 09/25] unwind_user/eh_frame: Add support for reading .eh_frame section
  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 15:05   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:05 UTC (permalink / raw)
  To: Jens Remus
  Cc: Christian Borntraeger, Heiko Carstens, linux-s390, Vasily Gorbik,
	linux-trace-kernel, Alexander Gordeev

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] `DW_CFA_restore` incorrectly clears register rules instead of restoring the CIE's initial rule.
- [High] Uninitialized usage of `fde->func_addr` when decoding an FDE's start address via `DW_EH_PE_funcrel`.
- [High] The `eh_frame_cfi_context` struct is partially initialized, exposing uninitialized stack memory via `cfa_offset`.
- [Low] Incompatible pointer type passed to `read_uleb128()` in `DW_CFA_offset` instruction parsing.
--

--- Patch [9]: [RFC PATCH v1 09/25] unwind_user/eh_frame: Add support for reading .eh_frame section ---
commit 7603506904ca3b69840f3790ec49f48b9d3d54fb
Author: Jens Remus <jremus@linux.ibm.com>

unwind_user/eh_frame: Add support for reading .eh_frame section

This patch adds an eh_frame_find interface to parse DWARF Call Frame
Information from the .eh_frame section for unwinding user space stacks. It
reads Common Information Entries, Frame Description Entries, and evaluates
DWARF CFI instructions to restore registers.

> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c
[ ... ]
>  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,
>                                                 unsigned long *value)
>  {
[ ... ]
> +	case DW_EH_PE_funcrel:
> +		if (!fde)
> +			return -EINVAL;
> +		result = fde->func_addr;
> +		break;
[ ... ]
> +static __always_inline int __read_fde(struct eh_frame_section *sec,
> +				      unsigned long fde_addr,
> +				      struct eh_frame_fde *fde)
> +{
[ ... ]
> +	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);

[Severity: High]
Does this code use an uninitialized value for the function address?

If the user-space eh_frame section specifies the DW_EH_PE_funcrel
encoding for fde_addr_enc, read_encoded_pointer() will attempt to evaluate
fde->func_addr.

However, fde->func_addr is not populated until the end of __read_fde(),
after the function address has already been parsed. Could this allow a
crafted eh_frame to use uninitialized kernel stack memory in its
address calculation?

> +		if (ret)
> +			return ret;
> +		if (func_addr < sec->text_start || func_addr >= sec->text_end)
> +			return -EINVAL;
[ ... ]
> +	fde->func_addr		= func_addr;
[ ... ]
> +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)
> +{
[ ... ]
> +	case DW_CFA_offset: {
> +		u8 reg = DW_CFA_operand(opcode);
> +		long offset;
> +		int idx;
> +
> +		ret = read_uleb128(&cur, end, &offset);

[Severity: Low]
Is the offset variable the correct type here?

The read_uleb128() function expects a pointer to an unsigned long, but
offset is defined as a signed long. This creates an incompatible pointer
type warning during the build.

> +		if (ret)
> +			return ret;
> +		offset *= fde->cie.data_align;
[ ... ]
> +	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;
> +	}

[Severity: High]
Does this instruction handler violate the DWARF specification?

The DWARF standard requires DW_CFA_restore to change the rule for the
indicated register back to the rule assigned by the initial instructions in
the CIE. By unconditionally setting it to REG_UNDEFINED_IMPLICIT instead
of restoring the CIE initial rule, this could corrupt the register recovery
state and cause unwinding to fail.

[ ... ]
> +		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;
> +		}
[ ... ]
> +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);

[Severity: High]
Can this leak kernel stack memory?

The eh_frame_cfi_context ctx structure is allocated on the stack without
being zero-initialized. In __do_cfi_program(), some fields are manually
initialized, but cfa_offset is omitted.

If an attacker provides a crafted eh_frame using DW_CFA_def_cfa_register
as the first state-modifying instruction, it changes cfa_rule to
CFA_REG_OFFSET without setting cfa_offset. The uninitialized kernel stack
garbage in ctx.state.cfa_offset is then subsequently copied into
frame->cfa.offset, allowing it to be used as an offset to read memory.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=9

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

* Re: [RFC PATCH v1 16/25] unwind_user/eh_frame: Add linear .eh_frame search fallback
  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 15:06   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:06 UTC (permalink / raw)
  To: Jens Remus
  Cc: Alexander Gordeev, linux-trace-kernel, Christian Borntraeger,
	Heiko Carstens, linux-s390, Vasily Gorbik

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] The fallback stub __find_fde_lsearch() returns 0 (success) instead of an error when CONFIG_EH_FRAME_LINEAR_SEARCH is disabled, causing the unwinder to consume an uninitialized stack variable.
- [High] Enabling CONFIG_EH_FRAME_LINEAR_SEARCH unconditionally triggers linear validation and causes out-of-bounds reads, permanently breaking unwinding for valid binaries lacking a zero terminator.
--

--- Patch [16]: [RFC PATCH v1 16/25] unwind_user/eh_frame: Add linear .eh_frame search fallback ---
commit 4fc15aee544b13c1c7ff56efb01c804156c21a55
Author: Jens Remus <jremus@linux.ibm.com>

unwind_user/eh_frame: Add linear .eh_frame search fallback

This commit adds a linear search fallback for .eh_frame sections when
the .eh_frame_hdr lacks a binary search table. It also introduces validation
logic for the referenced .eh_frame section, guarded by a new config
option EH_FRAME_LINEAR_SEARCH.

> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> index 7f572d1711d3f..ac288cec8021f 100644
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c
[ ... ]
> @@ -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) {

[Severity: High]
Since the section header only provides the start address, this loop iterates
all the way to vma_end. As noted in the commit message, many binaries lack
a zero terminator.

If an unresolved IP is scanned, will this read past the actual section
boundary into padding or other sections, return -EINVAL on garbage data, and
subsequently cause the caller to permanently remove the section?

[ ... ]
> +		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;
> +}

[Severity: Critical]
Does returning 0 here falsely indicate success when linear search is
disabled?

If a binary lacks a search table, __find_fde() will call this stub.
Returning 0 causes the caller, eh_frame_find(), to proceed with an
uninitialized fde stack variable:

kernel/unwind/eh_frame.c:eh_frame_find() {
    struct eh_frame_fde fde;
    ...
    ret = __find_fde(sec, ip, &fde);
    if (!ret)
        ret = __find_frame_row(sec, &fde, ip, frame);
    ...
}

Could this lead to corrupted stack traces or arbitrary memory reads by
consuming the uninitialized fde context? Should this return -ENOENT
instead?

[ ... ]
> @@ -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)
> +{
[ ... ]
> +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;
> +	}

[Severity: High]
Does this unconditionally apply linear validation to all binaries when
CONFIG_EH_FRAME_LINEAR_SEARCH is enabled, even if they already have a valid
binary search table?

If the binary lacks a zero terminator, eh_frame_validate_eh_frame() will
read out of bounds up to vma_end, fail with -EINVAL, and reject the entire
section.

Will this permanently break unwinding for valid binaries that were
previously working fine via the binary search table?

> +
> +	return 0;
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=16

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

* Re: [RFC PATCH v1 23/25] s390/ptrace: Provide frame_pointer()
  2026-08-18 14:49 ` [RFC PATCH v1 23/25] s390/ptrace: Provide frame_pointer() Jens Remus
@ 2026-08-18 15:06   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:06 UTC (permalink / raw)
  To: Jens Remus
  Cc: Alexander Gordeev, Christian Borntraeger, Vasily Gorbik,
	linux-trace-kernel, Heiko Carstens, linux-s390

> 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>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=23


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

* Re: [RFC PATCH v1 19/25] x86/uaccess: Add unsafe_copy_from_user() implementation
  2026-08-18 14:49 ` [RFC PATCH v1 19/25] x86/uaccess: Add unsafe_copy_from_user() implementation Jens Remus
@ 2026-08-18 15:08   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:08 UTC (permalink / raw)
  To: Jens Remus
  Cc: linux-trace-kernel, Heiko Carstens, Christian Borntraeger,
	Alexander Gordeev, linux-s390, Vasily Gorbik

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The `unsafe_copy_to_user` and `unsafe_copy_from_user` macros use dangerously common local variable names (`__dst`, `__src`, `__len`) which can silently shadow caller variables.
--

--- Patch [19]: [RFC PATCH v1 19/25] x86/uaccess: Add unsafe_copy_from_user() implementation ---
commit 83e5aa488f68698873050ec00671487d755924f5
Author: Josh Poimboeuf <jpoimboe@kernel.org>

x86/uaccess: Add unsafe_copy_from_user() implementation

This commit adds an x86 implementation of unsafe_copy_from_user() similar to
the existing unsafe_copy_to_user(). It also updates the local variables in
the unsafe_copy_to_user() macro.

> diff --git a/arch/x86/include/asm/uaccess.h b/arch/x86/include/asm/uaccess.h
> --- a/arch/x86/include/asm/uaccess.h
> +++ b/arch/x86/include/asm/uaccess.h

[ ... ]

> -#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);							\

[Severity: Medium]
Could these generic variable names (__dst, __src, __len) silently shadow
variables in the caller's scope?

If a caller passes a variable named __len as the _len argument, the macro
will expand to size_t __len = (__len);. 

This declares a new block-scoped variable and initializes it with its own
uninitialized garbage value from the stack. The copy loop would then use this
uninitialized length, potentially resulting in an out-of-bounds memory copy.

Does it make sense to retain a unique prefix like the previous __ucu_ variable
names to prevent macro shadowing?

> +	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(_dst, _src, _len, label)				\
> +do {										\
> +	void *__dst = (_dst);							\
> +	const void __user *__src = (_src);					\
> +	size_t __len = (_len);							\

[Severity: Medium]
Similarly, does the new unsafe_copy_from_user() macro share the same
shadowing vulnerability?

> +	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)

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=19

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

* Re: [RFC PATCH v1 14/25] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option
  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 15:08   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:08 UTC (permalink / raw)
  To: Jens Remus
  Cc: linux-s390, Christian Borntraeger, Alexander Gordeev,
	Heiko Carstens, linux-trace-kernel, Vasily Gorbik

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:

New issues:
- [High] Calling printk() inside a scoped_user_read_access_size block causes a UACCESS rule violation.
- [High] Infinite loop leading to local Denial of Service due to a 32-bit loop counter overflowing against a 64-bit upper bound.
- [Medium] CPU soft lockup due to unbounded loop over potentially millions of user-controlled entries without yielding.
- [Low] Compilation failure due to referencing a non-existent struct member `has_binary_search_table`.
- [Low] Spelling error in Kconfig help text ('entrire' instead of 'entire').

Pre-existing issues:
- [High] Information leak of uninitialized kernel stack memory to userspace via the unwinder.
--

--- Patch [14]: [RFC PATCH v1 14/25] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option ---
commit e7ce89dcff055122fac1a936bcef539a42d26943
Author: Jens Remus <jremus@linux.ibm.com>

unwind_user/eh_frame: Add .eh_frame[_hdr] validation option

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

> diff --git a/arch/Kconfig b/arch/Kconfig
> --- 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

[Severity: Low]
Is there a typo here? (entrire -> entire)

> +	  immediately rather than on demand.

[ ... ]

> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c

[ ... ]

> @@ -1107,11 +1107,11 @@
>  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;

[Severity: High]
This is a pre-existing issue, but can this leak uninitialized kernel stack
memory to userspace via the unwinder?

In __find_frame_row(), ctx is allocated on the kernel stack without
initialization. If an attacker provides a crafted .eh_frame starting with
DW_CFA_def_cfa_register, __do_cfi_program() sets cfa_rule = CFA_REG_OFFSET but
leaves ctx.state.cfa_offset uninitialized. This uninitialized kernel stack
value is assigned to frame->cfa.offset and used by the unwinder to compute the
user stack pointer.

[ ... ]

> @@ -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)

[Severity: Low]
Does this cause a build failure? It appears has_binary_search_table was never
added to struct eh_frame_section.

> +		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++) {

[Severity: High]
Can this loop become infinite and cause a hard lockup?

The loop counter i is a 32-bit unsigned int, while sec->fde_count is a 64-bit
unsigned long. If sec->fde_count exceeds UINT_MAX, i will wrap around to 0, and
the condition i < sec->fde_count will always evaluate to true.

[Severity: Medium]
Additionally, can this unbounded loop over potentially millions of entries
cause a CPU soft lockup or RCU stall? 

This loop executes entirely without a cond_resched() call, allowing a local
user to load a moderately large ELF file and hold the CPU uninterrupted for
seconds.

> +		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);

[Severity: High]
Does calling dbg_sec_ehfh() inside scoped_user_read_access_size violate UACCESS
rules?

The macro scoped_user_read_access_size enables UACCESS. dbg_sec_ehfh expands to
pr_debug, which calls printk. Calling complex or sleepable functions like
printk with UACCESS enabled can trigger page faults, take locks, or schedule,
potentially leading to kernel oopses or panics.

> +				return ret;
> +			}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=14

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

* Re: [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 08/25] unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree Jens Remus
@ 2026-08-18 15:08   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:08 UTC (permalink / raw)
  To: Jens Remus
  Cc: Alexander Gordeev, Christian Borntraeger, linux-s390,
	Heiko Carstens, Vasily Gorbik, linux-trace-kernel

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] The `eh_frame_mt` maple tree is initialized without `MT_FLAGS_USE_RCU`, causing internal nodes to be synchronously freed, which crashes concurrent lockless readers.
- [High] fork() fails to inherit .eh_frame_hdr registrations, breaking unwinding for child processes.
- [High] A TOCTOU race condition in `eh_frame_remove_section` allows a double-free of one section and a memory leak of another.
- [High] An integer overflow in `table_end` calculation allows maliciously crafted `.eh_frame_hdr` sections to bypass bounds checking.
- [Low] Appending INIT_MM_EH_FRAME in init_mm causes a missing comma syntax error on multiple architectures.
--

--- Patch [8]: [RFC PATCH v1 08/25] unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree ---
commit bd68e8b201de54350859df41e271e507dd497773
Author: Jens Remus <jremus@linux.ibm.com>
Subject: unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree

This commit associates .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.

> diff --git a/include/linux/eh_frame.h b/include/linux/eh_frame.h
> --- a/include/linux/eh_frame.h
> +++ b/include/linux/eh_frame.h
[ ... ]
> @@ -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),

[Severity: Critical]
Is the MT_FLAGS_USE_RCU flag required here?

Since readers are locklessly iterating the tree using mt_for_each() (which
relies on rcu_read_lock()), omitting MT_FLAGS_USE_RCU causes concurrent
mtree_erase() or mtree_insert_range() to synchronously free internal nodes via
kfree() instead of kfree_rcu(). Can this cause a use-after-free for the
concurrent readers?

> +extern void eh_frame_free_mm(struct mm_struct *mm);
> +
>  extern int eh_frame_add_section(unsigned long eh_frame_hdr_start,
[ ... ]
> diff --git a/kernel/fork.c b/kernel/fork.c
> --- a/kernel/fork.c
> +++ b/kernel/fork.c
[ ... ]
> @@ -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);

[Severity: Critical]
Should this use mt_init_flags() with MT_FLAGS_USE_RCU?

Similar to the INIT_MM_EH_FRAME macro, initializing without RCU could lead to
a use-after-free if internal maple tree nodes are freed while lockless readers
are iterating.

[Severity: High]
Does fork() fail to inherit .eh_frame_hdr registrations?

A process that registers unwinding information and then calls fork() will have
its VMAs copied in dup_mmap(), but the child receives this empty maple tree
without any copied eh_frame_mt entries. Will this break stack unwinding for
the child process?

> +#endif
> +}
> +
>  static struct mm_struct *mm_init(struct mm_struct *mm, struct task_struct *p)
[ ... ]
> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c
[ ... ]
> +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);
> +		}
> +	}

[Severity: High]
Can this mtree_erase() and call_srcu() sequence race with another thread and
cause a double-free?

If Thread A locklessly iterates and finds sec1 in mt_for_each(), and Thread B
concurrently removes sec1, Thread C could map a new section sec2 at the same
text_start address.

When Thread A calls __eh_frame_remove_section(mm, sec1), mtree_erase() would
successfully remove and return sec2. However, Thread A then passes the
original sec1 pointer to call_srcu(), double-freeing sec1 and leaking sec2.

[ ... ]
> diff --git a/mm/init-mm.c b/mm/init-mm.c
> --- a/mm/init-mm.c
> +++ b/mm/init-mm.c
> @@ -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
>  };

[Severity: Low]
Does appending INIT_MM_EH_FRAME here cause a missing comma syntax error on
some architectures?

While x86's INIT_MM_CONTEXT was updated to include a trailing comma in this
patch, other architectures (like UML or PowerPC Book3s 32-bit) may lack this
trailing comma in their INIT_MM_CONTEXT definitions, breaking the build.


In a related function, eh_frame_read_header() contains a potential overflow:

kernel/unwind/eh_frame.c:eh_frame_read_header() {
    ...
		table_end = table_start + fde_count * entry_size;
		if (table_end > sec->eh_frame_hdr_end)
			return -EINVAL;
    ...
}

[Severity: High]
Can this arithmetic multiplication overflow and bypass the bounds check?

If a malicious .eh_frame_hdr provides an artificially large fde_count,
fde_count * entry_size could overflow the unsigned long capacity, wrapping
around to a small value. This would bypass the bounds check and allow
out-of-bounds reads during later unwinding binary searches.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=8

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

* Re: [RFC PATCH v1 11/25] unwind_user/eh_frame: Wire up unwind_user to eh_frame
  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 15:09   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:09 UTC (permalink / raw)
  To: Jens Remus
  Cc: linux-trace-kernel, Alexander Gordeev, linux-s390, Heiko Carstens,
	Vasily Gorbik, Christian Borntraeger

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] The unwinder fails to adjust the return address before looking up the FDE for non-topmost frames, leading to unwinding failures or incorrect CFI state at function boundaries.
- [High] Child processes created via fork() do not inherit the .eh_frame tracking structures, silently breaking user stack unwinding for all child processes.
--

--- Patch [11]: [RFC PATCH v1 11/25] unwind_user/eh_frame: Wire up unwind_user to eh_frame ---
commit 409d12d48faaa34ee17f2272fc9cad1cbf48d5d0
Author: Jens Remus <jremus@linux.ibm.com>

unwind_user/eh_frame: Wire up unwind_user to eh_frame

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.

> diff --git a/kernel/unwind/user.c b/kernel/unwind/user.c
> index 830c620fe453c..85fc82252af15 100644
> --- a/kernel/unwind/user.c
> +++ b/kernel/unwind/user.c
[ ... ]
> @@ -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))

[Severity: High]
Does the lookup need to adjust the instruction pointer for non-topmost frames?

When state->ip holds the return address (for non-topmost frames), it points to
the instruction after the call. If a function call is the last instruction of
an FDE block, passing the unmodified state->ip could cause the lookup to fall
outside the FDE or into the next function's FDE.

Could this lead to incorrect unwinding state or unwinding failures at function
boundaries?

> +		return -ENOENT;
> +	return unwind_user_next_common(state, &frame);
> +}

[Severity: High]
Are the .eh_frame tracking structures properly inherited by child processes?

When a process is duplicated in dup_mmap() in kernel/fork.c, it seems the
parent's mm->eh_frame_mt maple tree is not duplicated. The child's tree is
initialized as completely empty in mm_init_eh_frame().

Will this silently break user stack unwinding for any child process created
via fork(), since unwinding attempts will search an empty tree and return
-ENOENT?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=11

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

* Re: [RFC PATCH v1 21/25] unwind_user/eh_frame/x86: Handle PLT expressions
  2026-08-18 14:49 ` [RFC PATCH v1 21/25] unwind_user/eh_frame/x86: Handle PLT expressions Jens Remus
@ 2026-08-18 15:10   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:10 UTC (permalink / raw)
  To: Jens Remus
  Cc: Christian Borntraeger, Heiko Carstens, Vasily Gorbik, linux-s390,
	Alexander Gordeev, linux-trace-kernel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The x86 PLT CFA expression handler hardcodes 64-bit DWARF register numbers, breaking the claimed i386 support.
--

--- Patch [21]: [RFC PATCH v1 21/25] unwind_user/eh_frame/x86: Handle PLT expressions ---
commit 0609783d6303290732b67ec7492d3c753e51d51c
Author: Jens Remus <jremus@linux.ibm.com>

unwind_user/eh_frame/x86: Handle PLT expressions

This commit adds x86-specific support for unwinding through GNU linker
generated PLT entries by handling the CFA expression found in .eh_frame.
It explicitly aims to enable this for both i386 and x86-64 binaries.

> diff --git a/arch/x86/include/asm/unwind_user_eh_frame.h b/arch/x86/include/asm/unwind_user_eh_frame.h
> index fdccbda2fe4b1..8268eb1a0ff5a 100644
> --- a/arch/x86/include/asm/unwind_user_eh_frame.h
> +++ b/arch/x86/include/asm/unwind_user_eh_frame.h
[ ... ]
> +static inline int eh_frame_do_def_cfa_expression(const char *expr,
> +						 int size,
> +						 unsigned long ip,
> +						 struct eh_frame_reg_state *reg_state)
> +{
[ ... ]
> +		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;

[Severity: Medium]
Does this unconditionally reject 32-bit i386 PLT expressions?

The commit message mentions enabling unwinding for both i386 and x86-64,
and the comment above notes the 32-bit DWARF registers 4 (ESP) and 8 (EIP).
However, this check requires the parsed DWARF register values to match
EH_FRAME_REG_SP (7) and EH_FRAME_REG_RA (16), which are exclusively the
x86-64 register mappings. 

Will this cause valid i386 expressions to fail and return -EOPNOTSUPP?

While 32-bit eh_frame unwinding is currently disabled at a higher level
for x86-64 tasks, this logic appears to contradict the intended architectural
support.

> +
> +		/* CFA = (SP + SP_offset) + (((IP & 0xf) >= N) << shift) */
> +		reg_state->cfa_rule = CFA_REG_OFFSET;
> +		reg_state->cfa_regnum = EH_FRAME_REG_SP;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=21

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

* Re: [RFC PATCH v1 22/25] unwind_user/eh_frame/x86: Handle DRAP expressions
  2026-08-18 14:49 ` [RFC PATCH v1 22/25] unwind_user/eh_frame/x86: Handle DRAP expressions Jens Remus
@ 2026-08-18 15:10   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:10 UTC (permalink / raw)
  To: Jens Remus
  Cc: Vasily Gorbik, Alexander Gordeev, linux-trace-kernel, linux-s390,
	Christian Borntraeger, Heiko Carstens

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Inner DRAP frames fail to unwind due to `state->topmost` restriction on `UNWIND_USER_RULE_REG_OFFSET_DEREF`.
--

--- Patch [22]: [RFC PATCH v1 22/25] unwind_user/eh_frame/x86: Handle DRAP expressions ---
commit a2eefb14435ba994b91a768aaf44090977190c84
Author: Jens Remus <jremus@linux.ibm.com>

unwind_user/eh_frame/x86: Handle DRAP expressions

GCC uses DRAP (Dynamic Realign Argument Pointer) when the stack needs
to be dynamically realigned. This patch adds x86-specific handling for
the CFA and FP DWARF expressions patterns used to describe how to unwind
through such frames.

> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> index 19e2bc96ddbc2..49e8a3e8d794a 100644
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c
[ ... ]
> @@ -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;

[Severity: High]
Will this rule cause unwinding to fail for inner stack frames?

When an inner stack frame requires DRAP evaluation, the unwinder evaluates
UNWIND_USER_RULE_REG_OFFSET_DEREF in kernel/unwind/user.c:unwind_user_next_common()
like this:

	if (!state->topmost || unwind_user_get_reg(&fp, frame->fp.regnum))
		return -EINVAL;

Since inner frames are not topmost, this check will return -EINVAL and
abort the unwinding process. Could this truncate backtraces that pass
through dynamically realigned stack frames?

> +		frame->fp.regnum = ctx.state.reg_regnum[FP_IDX];
> +		frame->fp.offset = ctx.state.reg_offset[FP_IDX];
> +		break;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=22

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

* Re: [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 12/25] unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption Jens Remus
@ 2026-08-18 15:10   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:10 UTC (permalink / raw)
  To: Jens Remus
  Cc: Christian Borntraeger, Alexander Gordeev, Vasily Gorbik,
	linux-s390, linux-trace-kernel, Heiko Carstens

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Lockless maple tree traversal races with concurrent in-place updates due to missing MT_FLAGS_USE_RCU.
--

--- Patch [12]: [RFC PATCH v1 12/25] unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption ---
commit 22bfd7679599379e32b650580760b87fb294aeb1
Author: Jens Remus <jremus@linux.ibm.com>

unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption

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.

> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> index 5ef8fef06a805..7657291324c06 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))
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

[Severity: High]
Could this introduce a race condition with concurrent lockless readers?

If multiple threads encounter a corrupted section concurrently, this will
trigger eh_frame_remove_section(), which eventually calls mtree_erase() on
mm->eh_frame_mt:

kernel/unwind/eh_frame.c:__eh_frame_remove_section() {
    ...
    if (!mtree_erase(&mm->eh_frame_mt, sec->text_start)) {
    ...
}

Because mm->eh_frame_mt is initialized in mm_init_eh_frame() without the
MT_FLAGS_USE_RCU flag:

kernel/fork.c:mm_init_eh_frame() {
    ...
    mt_init(&mm->eh_frame_mt);
    ...
}

mtree_erase() will modify the tree nodes in-place instead of using RCU-safe
copy-on-write.

Concurrently, eh_frame_find() performs a lockless traversal using mtree_load(),
which only relies on standard RCU for protection:

kernel/unwind/eh_frame.c:eh_frame_find() {
    ...
    sec = mtree_load(&mm->eh_frame_mt, ip);
    ...
}

Can concurrent readers observe torn state, freed nodes, or corrupted pivots
while the tree is being mutated in-place?

> +			dbg("eh_frame_remove_section() failed\n");
> +
>  	return ret;
>  }

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=12

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

* Re: [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 15/25] unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork Jens Remus
@ 2026-08-18 15:11   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:11 UTC (permalink / raw)
  To: Jens Remus
  Cc: linux-trace-kernel, Alexander Gordeev, Christian Borntraeger,
	linux-s390, Heiko Carstens, Vasily Gorbik

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] Mixing `guard(srcu)` with `goto`-based error cleanup in `eh_frame_dup_mm()` violates kernel cleanup guidelines.
- [Low] Compilation failure in `eh_frame_validate_section()` when `CONFIG_EH_FRAME_VALIDATION` is enabled.
--

--- Patch [15]: [RFC PATCH v1 15/25] unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork ---
commit 5bac3cc6bbe2eca53f4122c35113146b44210ee7
Author: Jens Remus <jremus@linux.ibm.com>

unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork

This commit ensures that a process's registered .eh_frame_hdr sections are
duplicated in the per-mm maple tree during virtual memory duplication. This
enables stack tracing for child processes using eh_frame.

> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> index c9161229196ce..7f572d1711d3f 100644
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c

[ ... ]

> +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;
> +}

[Severity: Low]
This isn't a bug, but does this function mix scope-based cleanup with
traditional goto-based error handling?

The kernel's cleanup guidelines specify that usage of goto and cleanup
helpers like guard() should not be mixed in the same function. Could this
be converted to use scope-based cleanup entirely, or avoid guard() to keep
the style consistent?

[ ... ]

Additionally, looking at eh_frame_validate_section() in the same file:

> 	if (!sec->has_binary_search_table)
> 		return 0;

[Severity: Low]
Can this cause a compilation failure when CONFIG_EH_FRAME_VALIDATION is
enabled?

It appears the function attempts to access sec->has_binary_search_table,
but this boolean member is not declared in struct eh_frame_section in
include/linux/eh_frame.h.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=15

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

* Re: [RFC PATCH v1 18/25] unwind_user/eh_frame: Add support for DWARF expressions
  2026-08-18 14:49 ` [RFC PATCH v1 18/25] unwind_user/eh_frame: Add support for DWARF expressions Jens Remus
@ 2026-08-18 15:13   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:13 UTC (permalink / raw)
  To: Jens Remus
  Cc: linux-s390, Vasily Gorbik, Alexander Gordeev,
	Christian Borntraeger, linux-trace-kernel, Heiko Carstens

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Critical] Using `unsafe_copy_from_user` inside a `scoped_user_read_access_size` block prematurely clears the AC flag on x86, causing subsequent user memory accesses to trigger a fatal Page Fault (kernel panic).
- [High] Integer overflow in the DWARF expression bounds check allows an infinite parsing loop in kernel space.
- [High] The `cfa_offset` field of the CFI context state is passed to the unwinder uninitialized if a malformed CFI program starts with a `DW_CFA_def_cfa_register` instruction.
- [Medium] Calling an out-of-line function within a UACCESS region violates objtool's safe execution rules.
--

--- Patch [18]: [RFC PATCH v1 18/25] unwind_user/eh_frame: Add support for DWARF expressions ---
commit 8c411d17e9c2d4af2ed33eb236e117fc360ae4f2
Author: Jens Remus <jremus@linux.ibm.com>
Subject: unwind_user/eh_frame: Add support for DWARF expressions

This commit enables architectures to handle DWARF expressions in DWARF CFI
instructions. It limits the maximum expression length to a reasonable size
and provides inline stubs for architectures to override.

> diff --git a/include/linux/unwind_user_eh_frame_types.h b/include/linux/unwind_user_eh_frame_types.h
> new file mode 100644
> --- /dev/null
> +++ b/include/linux/unwind_user_eh_frame_types.h
> @@ -0,0 +1,37 @@
[ ... ]
> +struct eh_frame_reg_state {
> +	/* CFA recovery rule */
> +	enum eh_frame_cfa_rule cfa_rule;
> +	unsigned long cfa_regnum;
> +	long cfa_offset;

[Severity: High]
What ensures cfa_offset is initialized when processing CFI programs?

When struct eh_frame_cfi_context ctx is allocated on the kernel stack in
__do_cfi_program(), its state.cfa_rule is initialized, but state.cfa_offset
is not.

If a user-supplied .eh_frame begins with DW_CFA_def_cfa_register, it sets
ctx.state.cfa_rule = CFA_REG_OFFSET but leaves cfa_offset untouched.

When __find_frame_row() executes, it verifies cfa_rule == CFA_REG_OFFSET
and unconditionally reads the uninitialized kernel stack memory. Could this
cause the unwinder to use unpredictable values for the 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];
> +};

[ ... ]

> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c
[ ... ]
> @@ -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)

[Severity: High]
Can the pointer arithmetic wrap around if expr_len is maliciously large?

Since expr_len is parsed from untrusted user input as an unsigned long, a
large value could overflow cur + expr_len and wrap to a small value,
bypassing this check against end.

If this occurs, the parsing loop would advance cur by the overflowed
expr_len, moving cur backwards. Would this lead to an infinite parsing loop
and a hard lockup?

> +				return -EINVAL;
> +
> +			if (expr_len > sizeof(expr))
> +				return -EOPNOTSUPP;
> +			unsafe_copy_from_user(&expr, (void __user *)cur, expr_len, Efault);

[Severity: Critical]
Might unsafe_copy_from_user() improperly toggle SMAP during expression
parsing?

Because this parsing runs inside a scoped_user_read_access_size block,
calling unsafe_copy_from_user() on x86 defaults to __copy_from_user(),
which executes its own stac() and clac() instructions.

The inner clac() instruction clears the AC flag prematurely, ending the
UACCESS region while the outer block assumes it is still open.

When the loop continues to parse the next instruction using
UNSAFE_GET_USER_INC, it will attempt a direct user memory read with the AC
flag cleared. Will this result in a fatal page fault?

> +			ret = eh_frame_do_def_cfa_expression(expr, expr_len, target_ip, &ctx->state);

[Severity: Medium]
Is it safe to call an architecture hook inside a UACCESS region?

If an architecture overrides eh_frame_do_def_cfa_expression() with an
out-of-line external function, it will trigger objtool's 'call to XXX()
with UACCESS enabled' warning.

Executing arbitrary functions with the AC flag set is strictly forbidden
on x86 to prevent SMAP bypasses.

> +			if (ret)
> +				return ret;
> +			cur += expr_len;
> +			break;
> +		}

[ ... ]

> @@ -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);

[Severity: Critical]
Does this copy run into the same SMAP clearing issue as mentioned above?

Calling unsafe_copy_from_user() here will also execute a clac() instruction
and clear the AC flag, causing subsequent user memory accesses to trigger a
fatal page fault.

> +				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);

[Severity: Medium]
Could these hooks also cause objtool UACCESS warnings?

Similar to eh_frame_do_def_cfa_expression(), overriding these with external
functions could lead to executing arbitrary code with the AC flag set.

> +				if (ret)
> +					return ret;
> +			}
>  			cur += expr_len;
>  			break;
>  		}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=18

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

* Re: [RFC PATCH v1 24/25] unwind_user/eh_frame/s390: Enable eh_frame unwinding on s390
  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 15:15   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:15 UTC (permalink / raw)
  To: Jens Remus
  Cc: linux-s390, Heiko Carstens, Alexander Gordeev,
	Christian Borntraeger, linux-trace-kernel, Vasily Gorbik

> 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>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=24


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

* Re: [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 25/25] unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections Jens Remus
@ 2026-08-18 15:17   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:17 UTC (permalink / raw)
  To: Jens Remus
  Cc: Alexander Gordeev, Heiko Carstens, Vasily Gorbik,
	Christian Borntraeger, linux-s390, linux-trace-kernel

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Critical] Race condition in `__eh_frame_remove_section()` leads to double-free, memory leak, and SRCU list corruption.
- [Critical] Unwinder error path triggers hard deadlock and NMI-safety violations by calling `eh_frame_remove_section()`.
- [Medium] Strict size matching in prctl handlers rejects UAPI structure extensions, breaking forward compatibility.
- [Medium] Silent data truncation of 64-bit `eh_frame_hdr_start` in `eh_frame_unregister()` on 32-bit platforms.
--

--- Patch [25]: [RFC PATCH v1 25/25] unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections ---
commit 04469d758625968e94108e567fd1bac500c0f41d
Author: Jens Remus <jremus@linux.ibm.com>

unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections

This patch adds two new prctl options to allow dynamic linkers to register
and unregister .eh_frame_hdr sections with the kernel. It introduces the
eh_frame_setup structure to pass section bounds from userspace and adds the
corresponding handlers.

Link: https://lore.kernel.org/all/20260528151023.00f5ec4e@gandalf.local.home/

> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> index 49e8a3e8d794a..12279f02381d0 100644
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c

[ ... ]

> @@ -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;

[Severity: Medium]
Could this strict size check prevent future UAPI extensions from being handled
properly by older kernels?

If a newer userspace binary passes a legitimately extended eh_frame_setup
struct to an older kernel, it looks like it will be rejected with -EINVAL.

Would it be better to use copy_struct_from_user() here to safely handle
zero-padded extensions?

> +
> +	if (copy_from_user(&data, user_data, sizeof(data)))
> +		return -EFAULT;

[ ... ]

> +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;

[Severity: Medium]
As with eh_frame_register(), does this strict size comparison break forward
compatibility for future struct extensions?

> +
> +	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);

[Severity: Medium]
Does this code silently truncate the 64-bit parameter on 32-bit platforms,
potentially causing the wrong section to be unregistered?

The data.eh_frame_hdr_start parameter is a __u64, but it is passed directly
into eh_frame_remove_section() which takes an unsigned long. If userspace
provides an out-of-bounds 64-bit value on a 32-bit system, it appears it
would be implicitly truncated to a valid 32-bit address rather than being
rejected.

Looking at the underlying implementation of eh_frame_remove_section() that is
exposed by this new prctl, is there a race condition in
__eh_frame_remove_section() that could lead to a double-free?

kernel/unwind/eh_frame.c:__eh_frame_remove_section() {
    ...
    if (!mtree_erase(&mm->eh_frame_mt, sec->text_start)) {
        dbg_sec("mtree_erase failed: text=%lx\n", sec->text_start);
        return -EINVAL;
    }
    
    call_srcu(&eh_frame_srcu, &sec->rcu, eh_frame_free_srcu);
    ...
}

[Severity: Critical]
If a thread unregisters the section and a third thread immediately registers
a new section at the exact same address, would mtree_erase() erase and return
the new section instead?

Because the code evaluates the return value simply as non-NULL, it seems it
would proceed to call call_srcu() on the old section again, potentially
causing a double-free and SRCU list corruption.

Also, regarding the interaction with the unwinder fast path in eh_frame_find():

kernel/unwind/eh_frame.c:eh_frame_find() {
    ...
    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))
    ...
}

[Severity: Critical]
Can this sequence cause a hard deadlock or NMI-safety violation?

Stack unwinding is frequently invoked from NMI or IRQ contexts. If
eh_frame_find() encounters a parsing error, it unconditionally calls
eh_frame_remove_section(). This attempts to acquire the mtree_lock spinlock
and invokes call_srcu(), both of which are forbidden in NMI/IRQ contexts.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=25

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

* Re: [RFC PATCH v1 10/25] unwind_user/eh_frame: Detect .eh_frame_hdr sections in executables
  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 15:18   ` sashiko-bot
  0 siblings, 0 replies; 51+ messages in thread
From: sashiko-bot @ 2026-08-18 15:18 UTC (permalink / raw)
  To: Jens Remus
  Cc: Alexander Gordeev, linux-trace-kernel, Christian Borntraeger,
	linux-s390, Heiko Carstens, Vasily Gorbik

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Missing duplication of eh_frame_mt during fork() leaves child processes without stack unwind capability.
- [Medium] Executable segments with a trailing BSS section silently fail .eh_frame_hdr registration.
--

--- Patch [10]: [RFC PATCH v1 10/25] unwind_user/eh_frame: Detect .eh_frame_hdr sections in executables ---
commit e178ff4c7b227a1dcb3db08f8923210e443187ac
Author: Jens Remus <jremus@linux.ibm.com>

unwind_user/eh_frame: Detect .eh_frame_hdr sections in executables

This commit modifies the ELF loader to automatically detect .eh_frame_hdr
sections during binary and interpreter loading. It associates these sections
with the mm_struct to support user-space stack unwinding.

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

[Severity: High]
Are these eh_frame sections properly preserved for child processes during
fork()?

When a process forks, dup_mm() creates a new mm_struct and calls mm_init(),
which appears to initialize eh_frame_mt as an empty maple tree:

kernel/fork.c:mm_init_eh_frame() {
    ...
    mt_init(&mm->eh_frame_mt);
    ...
}

Is there any subsequent logic that duplicates the parent's eh_frame_mt
entries during dup_mm()? If not, won't child processes inherit correctly
duplicated VMAs but an empty eh_frame_mt, resulting in a complete loss of
stack unwinding capabilities for all children?

> diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c
> index 16a56b6b3f6ca..8c1cdd8318c53 100644
> --- a/fs/binfmt_elf.c
> +++ b/fs/binfmt_elf.c

[ ... ]

> @@ -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;

[Severity: Medium]
Will this silently fail to register .eh_frame_hdr for segments with a
trailing BSS section?

When an ELF executable segment has p_memsz > p_filesz crossing a page
boundary, the loader maps the file-backed portion and creates a separate
anonymous VMA for the BSS.

Because text_end is calculated using p_memsz, it may extend beyond the
file-backed VMA boundary. When this is passed to eh_frame_add_section(),
it uses vma_lookup() and checks if the entire segment resides in a single
VMA:

kernel/unwind/eh_frame.c:eh_frame_add_section() {
    ...
    if (!text_vma ||
        !(text_vma->vm_flags & VM_EXEC) ||
        text_end > text_vma->vm_end) {
        return -EINVAL;
    ...
}

Since the file-backed VMA ends before text_end in this scenario, won't the
text_end > text_vma->vm_end check fail and terminate registration?

> +
> +	/* Ignore return value, eh_frame[_hdr] section isn't critical */
> +	eh_frame_add_section(eh_frame_start, eh_frame_end, text_start, text_end);
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818144954.2320378-1-jremus@linux.ibm.com?part=10

^ permalink raw reply	[flat|nested] 51+ 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; 51+ 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] 51+ messages in thread

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

Thread overview: 51+ 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:56   ` sashiko-bot
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:58   ` sashiko-bot
2026-08-18 14:49 ` [RFC PATCH v1 04/25] unwind_user: Flexible FP/RA recovery rules Jens Remus
2026-08-18 14:58   ` sashiko-bot
2026-08-18 14:49 ` [RFC PATCH v1 05/25] unwind_user: Flexible CFA " Jens Remus
2026-08-18 14:57   ` sashiko-bot
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:57   ` sashiko-bot
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 15:02   ` sashiko-bot
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 15:08   ` sashiko-bot
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 15:05   ` sashiko-bot
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 15:18   ` sashiko-bot
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 15:09   ` sashiko-bot
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 15:10   ` sashiko-bot
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 15:00   ` sashiko-bot
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 15:08   ` sashiko-bot
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 15:11   ` sashiko-bot
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 15:06   ` sashiko-bot
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 15:04   ` sashiko-bot
2026-08-18 14:49 ` [RFC PATCH v1 18/25] unwind_user/eh_frame: Add support for DWARF expressions Jens Remus
2026-08-18 15:13   ` sashiko-bot
2026-08-18 14:49 ` [RFC PATCH v1 19/25] x86/uaccess: Add unsafe_copy_from_user() implementation Jens Remus
2026-08-18 15:08   ` sashiko-bot
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 15:04   ` sashiko-bot
2026-08-18 14:49 ` [RFC PATCH v1 21/25] unwind_user/eh_frame/x86: Handle PLT expressions Jens Remus
2026-08-18 15:10   ` sashiko-bot
2026-08-18 14:49 ` [RFC PATCH v1 22/25] unwind_user/eh_frame/x86: Handle DRAP expressions Jens Remus
2026-08-18 15:10   ` sashiko-bot
2026-08-18 14:49 ` [RFC PATCH v1 23/25] s390/ptrace: Provide frame_pointer() Jens Remus
2026-08-18 15:06   ` sashiko-bot
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 15:15   ` sashiko-bot
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 15:17   ` sashiko-bot
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