* [RFC PATCH v2 01/24] unwind_user: Add generic and arch-specific headers to MAINTAINERS
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 19:52 ` [RFC PATCH v2 02/24] unwind_user: Stop when reaching an outermost frame Jens Remus
` (22 subsequent siblings)
23 siblings, 0 replies; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song, 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] 48+ messages in thread* [RFC PATCH v2 02/24] unwind_user: Stop when reaching an outermost frame
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
2026-08-21 19:52 ` [RFC PATCH v2 01/24] unwind_user: Add generic and arch-specific headers to MAINTAINERS Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:00 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 03/24] unwind_user: Enable archs that pass RA in a register Jens Remus
` (21 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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] 48+ messages in thread* [RFC PATCH v2 03/24] unwind_user: Enable archs that pass RA in a register
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
2026-08-21 19:52 ` [RFC PATCH v2 01/24] unwind_user: Add generic and arch-specific headers to MAINTAINERS Jens Remus
2026-08-21 19:52 ` [RFC PATCH v2 02/24] unwind_user: Stop when reaching an outermost frame Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:02 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 04/24] unwind_user: Flexible FP/RA recovery rules Jens Remus
` (20 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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] 48+ messages in thread* Re: [RFC PATCH v2 03/24] unwind_user: Enable archs that pass RA in a register
2026-08-21 19:52 ` [RFC PATCH v2 03/24] unwind_user: Enable archs that pass RA in a register Jens Remus
@ 2026-08-21 20:02 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:02 UTC (permalink / raw)
To: Jens Remus
Cc: linux-s390, linux-trace-kernel, Heiko Carstens, Vasily Gorbik,
Alexander Gordeev, Christian Borntraeger
> 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/20260821195259.2688377-1-jremus@linux.ibm.com?part=3
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 04/24] unwind_user: Flexible FP/RA recovery rules
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (2 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 03/24] unwind_user: Enable archs that pass RA in a register Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:03 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 05/24] unwind_user: Flexible CFA " Jens Remus
` (19 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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] 48+ messages in thread* Re: [RFC PATCH v2 04/24] unwind_user: Flexible FP/RA recovery rules
2026-08-21 19:52 ` [RFC PATCH v2 04/24] unwind_user: Flexible FP/RA recovery rules Jens Remus
@ 2026-08-21 20:03 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:03 UTC (permalink / raw)
To: Jens Remus
Cc: Christian Borntraeger, Heiko Carstens, linux-trace-kernel,
Vasily Gorbik, linux-s390, 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/20260821195259.2688377-1-jremus@linux.ibm.com?part=4
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 05/24] unwind_user: Flexible CFA recovery rules
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (3 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 04/24] unwind_user: Flexible FP/RA recovery rules Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:03 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 06/24] unwind_user: Enable archs that define CFA = SP_callsite + offset Jens Remus
` (18 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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] 48+ messages in thread* [RFC PATCH v2 06/24] unwind_user: Enable archs that define CFA = SP_callsite + offset
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (4 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 05/24] unwind_user: Flexible CFA " Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:03 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 07/24] unwind_user/eh_frame: Add support for reading .eh_frame_hdr section Jens Remus
` (17 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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] 48+ messages in thread* Re: [RFC PATCH v2 06/24] unwind_user: Enable archs that define CFA = SP_callsite + offset
2026-08-21 19:52 ` [RFC PATCH v2 06/24] unwind_user: Enable archs that define CFA = SP_callsite + offset Jens Remus
@ 2026-08-21 20:03 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:03 UTC (permalink / raw)
To: Jens Remus
Cc: linux-s390, linux-trace-kernel, Heiko Carstens,
Christian Borntraeger, Alexander Gordeev, Vasily Gorbik
> 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/20260821195259.2688377-1-jremus@linux.ibm.com?part=6
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 07/24] unwind_user/eh_frame: Add support for reading .eh_frame_hdr section
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (5 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 06/24] unwind_user: Enable archs that define CFA = SP_callsite + offset Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:06 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 08/24] unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree Jens Remus
` (16 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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>
---
Notes (jremus):
Changes in RFC v2:
- read_encoded_pointer(): Enable DW_EH_PE_absptr in combination with
relative application flags (e.g. DW_EH_PE_pcrel). (Sashiko AI)
- eh_frame_read_header(): Fix binary search table bounds check.
(Sashiko AI)
- UNSAFE_GET_USER_INC(): Improve overflow check.
MAINTAINERS | 1 +
arch/Kconfig | 3 +
include/linux/eh_frame.h | 45 +++++
kernel/unwind/Makefile | 3 +-
kernel/unwind/eh_frame.c | 366 +++++++++++++++++++++++++++++++++++++++
kernel/unwind/eh_frame.h | 30 ++++
6 files changed, 447 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..f25a0923e315
--- /dev/null
+++ b/kernel/unwind/eh_frame.c
@@ -0,0 +1,366 @@
+// 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 (sizeof(__to) > end - from) \
+ 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;
+ int ret;
+
+ if (encoding == DW_EH_PE_omit)
+ return -EINVAL;
+
+ /* Determine base address based on application */
+ switch (application) {
+ case 0:
+ /* Absolute */
+ result = 0;
+ 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: {
+ unsigned long tmp;
+ UNSAFE_GET_USER_INC(tmp, cur, end, Efault);
+ result += tmp;
+ 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;
+ if (fde_count > (end - table_start) / entry_size)
+ return -EINVAL;
+ table_end = table_start + fde_count * entry_size;
+ }
+
+ 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] 48+ messages in thread* Re: [RFC PATCH v2 07/24] unwind_user/eh_frame: Add support for reading .eh_frame_hdr section
2026-08-21 19:52 ` [RFC PATCH v2 07/24] unwind_user/eh_frame: Add support for reading .eh_frame_hdr section Jens Remus
@ 2026-08-21 20:06 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:06 UTC (permalink / raw)
To: Jens Remus
Cc: Heiko Carstens, Alexander Gordeev, linux-trace-kernel,
Christian Borntraeger, linux-s390, Vasily Gorbik
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Incorrect pointer size assumption for 32-bit compat tasks using DW_EH_PE_absptr encoding.
- [High] Bypassing speculative execution pointer masking by using an unmasked pointer within a `scoped_user_read_access_size` block.
- [Low] Mixing goto-based cleanup labels and scoped_guard declarations in the same function.
--
--- Patch [7]: [RFC PATCH v2 07/24] unwind_user/eh_frame: Add support for reading .eh_frame_hdr section ---
commit d29ad7450daa52f7e2ed07991186d7ccf95beadb
Author: Jens Remus <jremus@linux.ibm.com>
unwind_user/eh_frame: Add support for reading .eh_frame_hdr section
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.
> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> new file mode 100644
> index 0000000000000..f25a0923e315b
> --- /dev/null
> +++ b/kernel/unwind/eh_frame.c
> @@ -0,0 +1,366 @@
[ ... ]
> +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);
[Severity: High]
Does hardcoding sizeof(unsigned long) here cause issues for 32-bit compat
tasks?
If a 64-bit kernel is profiling a 32-bit application, DW_EH_PE_absptr is
expected to match the application's native 4-byte pointer size. Using
sizeof(unsigned long) means the kernel will read 8 bytes, which could
consume too much data and misalign the current pointer for the remainder
of the unwinding process.
> + case DW_EH_PE_udata2:
> + case DW_EH_PE_sdata2:
> + return 2;
[ ... ]
> +static __always_inline int read_encoded_pointer(struct eh_frame_section *sec,
> + unsigned long *addr,
> + unsigned long end,
> + u8 encoding,
> + unsigned long *value)
> +{
[ ... ]
> + /* Read value based on format */
> + switch (format) {
> + case DW_EH_PE_absptr: {
> + unsigned long tmp;
> + UNSAFE_GET_USER_INC(tmp, cur, end, Efault);
> + result += tmp;
> + break;
> + }
[Severity: High]
Will this also read 8 bytes instead of 4 for a 32-bit task?
Similar to encoded_pointer_size(), this reads a full unsigned long for an
absolute pointer. If this is a 64-bit kernel unwinding a 32-bit compat
application, it seems this might read past the intended data boundaries
and corrupt the address parsing.
> + case DW_EH_PE_uleb128: {
> + unsigned long tmp;
[ ... ]
> +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;
[ ... ]
> + 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);
[Severity: High]
Does this bypass the speculative execution pointer masking provided by
scoped_user_read_access_size()?
The block masks the eh_frame_hdr pointer, but inside the block,
UNSAFE_GET_USER_INC() reads from the unmasked unsigned long cur variable:
#define UNSAFE_GET_USER_INC(to, from, end, label)
...
unsafe_get_user(__to, (typeof(to) __user *)from, label);
Since cur is passed to unsafe_get_user() without being masked, it appears
this defeats the Spectre V1 mitigation for architectures relying on
can_do_masked_user_access().
> + if (version != 1)
> + return -EINVAL;
[ ... ]
> +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;
[ ... ]
> + 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);
[ ... ]
> + 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;
> +}
[Severity: Low]
This isn't a bug, but should this function avoid mixing scoped_guard() and
goto-based error handling?
The kernel cleanup guidelines recommend either fully converting to
scope-based cleanup (for example, using __free() for the sec allocation)
or strictly using goto-based cleanups without scoped_guard(). Mixing them
in the same function can create confusing ownership semantics.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821195259.2688377-1-jremus@linux.ibm.com?part=7
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 08/24] unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (6 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 07/24] unwind_user/eh_frame: Add support for reading .eh_frame_hdr section Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:13 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 09/24] unwind_user/eh_frame: Add support for reading .eh_frame section Jens Remus
` (15 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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>
---
Notes (jremus):
Changes in RFC v2:
- INIT_MM_EH_FRAME and mm_init_eh_frame(): Use MT_FLAGS_USE_RCU to
enable concurrent eh_frame_mt read and write access. (Sashiko AI)
- eh_frame_remove_section(): Fix ABA problem by using the advanced
maple tree API to hold the internal spinlock while iterating over
the eh_frame_mt and removing the eh_frame section(s). (Sashiko AI)
arch/x86/include/asm/mmu.h | 2 +-
include/linux/eh_frame.h | 22 ++++++++++++
include/linux/mm_types.h | 3 ++
kernel/fork.c | 10 ++++++
kernel/unwind/eh_frame.c | 70 ++++++++++++++++++++++++++++++++++++--
mm/init-mm.c | 2 ++
6 files changed, 105 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..c4279ee77c13 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,29 @@ struct eh_frame_section {
u8 binary_search_table_enc;
};
+#define EH_FRAME_MT_FLAGS (MT_FLAGS_USE_RCU)
+
+#define INIT_MM_EH_FRAME .eh_frame_mt = MTREE_INIT(eh_frame_mt, EH_FRAME_MT_FLAGS),
+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 +60,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..dab67d2eb096 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_flags(&mm->eh_frame_mt, EH_FRAME_MT_FLAGS);
+#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 f25a0923e315..8cf19c5298fd 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; \
@@ -310,6 +312,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 = ¤t->mm->eh_frame_mt;
struct mm_struct *mm = current->mm;
struct eh_frame_section *sec;
int ret;
@@ -352,15 +355,76 @@ 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 ma_state *mas,
+ struct eh_frame_section *sec)
+{
+ if (mas_erase(mas) != sec) {
+ dbg("mas_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;
+ MA_STATE(mas, &mm->eh_frame_mt, 0, 0);
+ bool found = false;
+ int ret = 0;
+
+ guard(srcu)(&eh_frame_srcu);
+
+ mtree_lock(&mm->eh_frame_mt);
+ mas_for_each(&mas, sec, ULONG_MAX) {
+ if (sec->eh_frame_hdr_start == eh_frame_hdr_start) {
+ found = true;
+ ret |= __eh_frame_remove_section(&mas, sec);
+ }
+ }
+ mtree_unlock(&mm->eh_frame_mt);
+
+ 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] 48+ messages in thread* Re: [RFC PATCH v2 08/24] unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree
2026-08-21 19:52 ` [RFC PATCH v2 08/24] unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree Jens Remus
@ 2026-08-21 20:13 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:13 UTC (permalink / raw)
To: Jens Remus
Cc: linux-s390, Heiko Carstens, Alexander Gordeev, linux-trace-kernel,
Vasily Gorbik, Christian Borntraeger
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] If mas_erase() fails due to an Out-Of-Memory (OOM) condition, it leaves the entry in the tree but __eh_frame_remove_section() still frees it via call_srcu().
- [Medium] Mixing `goto`-based error handling with scope-based cleanup helpers in the same function.
- [Low] `efi_mm` is missing the `INIT_MM_EH_FRAME` initializer.
--
--- Patch [8]: [RFC PATCH v2 08/24] unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree ---
commit 428d78732477ec23211b2b4732a9923dae762c55
Author: Jens Remus <jremus@linux.ibm.com>
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_struct by
adding them to a per-mm maple tree. The tree is indexed by the
corresponding text address ranges.
> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> index f25a0923e315b..8cf19c5298fde 100644
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c
[ ... ]
> @@ -352,15 +355,76 @@ 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:
[Severity: Medium]
Is it intentional to mix goto-based error handling with scope-based cleanup
helpers in the same function?
The eh_frame_add_section function uses scoped_guard for the mmap lock but
retains a goto err_free label for allocation cleanup on failure.
The cleanup subsystem guidelines suggest that a function should either convert
all resources to scope-based cleanup or convert none of them to avoid
confusing ownership semantics.
> free_section(sec);
> return ret;
> }
[ ... ]
> +static int __eh_frame_remove_section(struct ma_state *mas,
> + struct eh_frame_section *sec)
> +{
> + if (mas_erase(mas) != sec) {
> + dbg("mas_erase failed: text=%lx\n", sec->text_start);
> + return -EINVAL;
> + }
> +
> + call_srcu(&eh_frame_srcu, &sec->rcu, eh_frame_free_srcu);
[Severity: Critical]
Can this cause a use-after-free if the maple tree erase operation fails?
If mas_erase fails due to memory exhaustion (for example, during tree
rebalancing), it leaves the entry in the tree, sets mas_is_err(mas), and
returns the pointer to the entry.
Because it returns the entry pointer, the condition mas_erase(mas) != sec
will evaluate to false, skipping the error block.
The code would then proceed to call call_srcu and free the section while
it remains linked in the maple tree, potentially causing a crash when the
tree is traversed later. Should this check mas_is_err(mas) instead?
> +
> + return 0;
> +}
[ ... ]
> diff --git a/mm/init-mm.c b/mm/init-mm.c
> index 3e792aad76261..25c26341fc4fb 100644
> --- 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 efi_mm in drivers/firmware/efi/efi.c also need this initializer?
When a new initializer like INIT_MM_EH_FRAME is added to init_mm, it usually
needs to be added to all static mm_struct definitions.
Leaving it out means the eh_frame_mt maple tree inside efi_mm remains
zero-initialized rather than properly initialized with MTREE_INIT, which
might lead to lock splats or crashes if accessed.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821195259.2688377-1-jremus@linux.ibm.com?part=8
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 09/24] unwind_user/eh_frame: Add support for reading .eh_frame section
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (7 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 08/24] unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:16 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 10/24] unwind_user/eh_frame: Detect .eh_frame_hdr sections in executables Jens Remus
` (14 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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>
---
Notes (jremus):
Changes in RFC v2:
- Reduce maximum state stack depth to 1 (EH_FRAME_MAX_STATE_STACK).
- Limit FDE length to 32768 (EH_FRAME_MAX_FDE_LENGTH).
- Limit number of CFI instructions processed to 16.384
(EH_FRAME_CFI_INSN_LIMIT) to prevent an infinite loop, in case of a
logic error that would enable malicious .eh_frame to make the cursor
go backwards (e.g. due to an overflow). (Sashiko AI)
[cc1plus FDE with 13.888 CFI insns]
- __read_fde(): Do not allow function-relative encoding when reading
PC begin (function start address) and PC range (function size) to
fix use of uninitialized function start address. (Sashiko AI)
- read_encoded_pointer(): Drop support for function-relative encoding,
as it is no longer used.
- DW_CFA_offset: Read offset into unsigned long. (Sashiko AI)
- Add support for DW_CFA_restore_extended.
- DW_CFA_def_cfa_{offset[_sf]|register}: Reject if CFA rule is not
CFA_REG_OFFSET to ensure the unchanged part of the rule is
initialized. (Sashiko AI)
- Improve CIE/FDE augmentation data length overflow checks.
- __do_cfi_insn(): Use check_mul_overflow() to check for overflow
in multiplications with CIE data alignment factor. (Sashiko AI)
- DW_CFA_restore[_extended]: Restore to CIE state. (Sashiko AI)
MAINTAINERS | 2 +
include/asm-generic/Kbuild | 1 +
include/asm-generic/unwind_user_eh_frame.h | 51 ++
include/linux/eh_frame.h | 7 +
kernel/unwind/eh_frame.c | 988 ++++++++++++++++++++-
kernel/unwind/eh_frame.h | 38 +
kernel/unwind/eh_frame_debug.h | 19 +
7 files changed, 1103 insertions(+), 3 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..da238557605b
--- /dev/null
+++ b/include/asm-generic/unwind_user_eh_frame.h
@@ -0,0 +1,51 @@
+/* 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_FDE_LENGTH
+#define EH_FRAME_MAX_FDE_LENGTH 32768
+#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 1
+#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_CFI_INSN_LIMIT
+#define EH_FRAME_CFI_INSN_LIMIT 16384
+#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 c4279ee77c13..740316b19df3 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
@@ -34,6 +35,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)
{
@@ -60,6 +62,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 8cf19c5298fd..21f40805b417 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -13,14 +13,85 @@
#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];
+ struct eh_frame_reg_state cie_state;
+ int stack_depth;
+ bool cie;
+};
+
+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; \
@@ -223,6 +294,917 @@ 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;
+ 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 < cie_addr || 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 */
+ aug_data_end = cur;
+ 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 < cur || 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, &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;
+ }
+ }
+ if (cur != aug_data_end)
+ return -EINVAL;
+ }
+
+ 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;
+ u8 range_enc;
+ 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 || length > EH_FRAME_MAX_FDE_LENGTH)
+ return -EINVAL;
+ end = fde_addr + 4 + length;
+ if (end < fde_addr || 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, &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 */
+ range_enc = DW_EH_PE_format(fde->cie.fde_addr_enc);
+ ret = read_encoded_pointer(sec, &cur, end, range_enc, &func_size);
+ if (ret)
+ return ret;
+ if (func_addr + func_size < func_addr || 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;
+ unsigned long aug_data_end;
+
+ ret = read_uleb128(&cur, end, &aug_data_len);
+ if (ret)
+ return ret;
+ aug_data_end = cur + aug_data_len;
+ if (aug_data_end < cur || aug_data_end > end)
+ return -EINVAL;
+ cur = aug_data_end;
+ }
+ }
+
+ 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, &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, &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);
+ unsigned long _offset;
+ long offset;
+ int idx;
+
+ ret = read_uleb128(&cur, end, &_offset);
+ if (ret)
+ return ret;
+ if (check_mul_overflow(_offset, fde->cie.data_align, &offset))
+ return -EINVAL;
+
+ 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;
+
+ if (ctx->cie)
+ return -EINVAL;
+
+ idx = reg_to_index(reg);
+ if (idx >= 0)
+ ctx->state.reg_rule[idx] = ctx->cie_state.reg_rule[idx];
+ 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, ®);
+ 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, ®);
+ if (ret)
+ return ret;
+ ret = read_sleb128(&cur, end, &offset);
+ if (ret)
+ return ret;
+ if (check_mul_overflow(offset, fde->cie.data_align, &offset))
+ return -EINVAL;
+
+ 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;
+
+ if (ctx->state.cfa_rule != CFA_REG_OFFSET)
+ return -EINVAL;
+
+ ret = read_uleb128(&cur, end, ®);
+ if (ret)
+ return ret;
+
+ ctx->state.cfa_regnum = reg;
+ break;
+ }
+
+ case DW_CFA_def_cfa_offset: {
+ unsigned long offset;
+
+ if (ctx->state.cfa_rule != CFA_REG_OFFSET)
+ return -EINVAL;
+
+ 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;
+
+ if (ctx->state.cfa_rule != CFA_REG_OFFSET)
+ return -EINVAL;
+ ret = read_sleb128(&cur, end, &offset);
+ if (ret)
+ return ret;
+ if (check_mul_overflow(offset, fde->cie.data_align, &offset))
+ return -EINVAL;
+
+ ctx->state.cfa_offset = offset;
+ break;
+ }
+
+ case DW_CFA_restore_extended: {
+ unsigned long reg;
+ int idx;
+
+ if (ctx->cie)
+ return -EINVAL;
+
+ ret = read_uleb128(&cur, end, ®);
+ if (ret)
+ return ret;
+
+ idx = reg_to_index(reg);
+ if (idx >= 0)
+ ctx->state.reg_rule[idx] = ctx->cie_state.reg_rule[idx];
+ break;
+ }
+
+ case DW_CFA_undefined: {
+ unsigned long reg;
+ int idx;
+
+ ret = read_uleb128(&cur, end, ®);
+ 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, ®);
+ 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, ®);
+ if (ret)
+ return ret;
+ ret = read_uleb128(&cur, end, &_offset);
+ if (ret)
+ return ret;
+ if (check_mul_overflow(_offset, fde->cie.data_align, &offset))
+ return -EINVAL;
+
+ 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, ®);
+ if (ret)
+ return ret;
+ ret = read_sleb128(&cur, end, &offset);
+ if (ret)
+ return ret;
+ if (check_mul_overflow(offset, fde->cie.data_align, &offset))
+ return -EINVAL;
+
+ 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, ®);
+ if (ret)
+ return ret;
+ ret = read_uleb128(&cur, end, &_offset);
+ if (ret)
+ return ret;
+ if (check_mul_overflow(_offset, fde->cie.data_align, &offset))
+ return -EINVAL;
+
+ 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, ®);
+ if (ret)
+ return ret;
+ ret = read_sleb128(&cur, end, &offset);
+ if (ret)
+ return ret;
+ if (check_mul_overflow(offset, fde->cie.data_align, &offset))
+ return -EINVAL;
+
+ 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, ®1);
+ if (ret)
+ return ret;
+ ret = read_uleb128(&cur, end, ®2);
+ 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, ®);
+ 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;
+ unsigned int insn_count = 0;
+ 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) */
+ ctx->cie = true;
+ 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) {
+ if (insn_count++ >= EH_FRAME_CFI_INSN_LIMIT)
+ return -EINVAL;
+ ret = __do_cfi_insn(sec, fde, &cur, fde->cie.cfi_insn_end, &ip, target_ip, ctx);
+ if (ret)
+ return ret;
+ }
+ }
+
+ /* Save CIE state for DW_CFA_restore[_extended] */
+ ctx->cie_state = ctx->state;
+
+ /* Do not allow remember/restore between CIE and FDE */
+ ctx->stack_depth = 0;
+
+ /* Process FDE CFI instructions up to target IP */
+ ctx->cie = false;
+ 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) {
+ if (insn_count++ >= EH_FRAME_CFI_INSN_LIMIT)
+ return -EINVAL;
+ 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);
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] 48+ messages in thread* Re: [RFC PATCH v2 09/24] unwind_user/eh_frame: Add support for reading .eh_frame section
2026-08-21 19:52 ` [RFC PATCH v2 09/24] unwind_user/eh_frame: Add support for reading .eh_frame section Jens Remus
@ 2026-08-21 20:16 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:16 UTC (permalink / raw)
To: Jens Remus
Cc: Heiko Carstens, linux-trace-kernel, Alexander Gordeev,
Vasily Gorbik, Christian Borntraeger, linux-s390
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] DW_CFA_restore and DW_CFA_restore_extended fail to restore the complete register rule state (offset and register number).
- [High] Bypass of pointer masking mitigation inside scoped_user_read_access_size blocks leading to a Spectre V1 vulnerability.
- [Medium] Integer overflow during instruction pointer advancement in DW_CFA_advance_loc4.
--
--- Patch [9]: [RFC PATCH v2 09/24] unwind_user/eh_frame: Add support for reading .eh_frame section ---
commit cd03be3d9017de830eb9301ff0da6dd6d08e69be
Author: Jens Remus <jremus@linux.ibm.com>
unwind_user/eh_frame: Add support for reading .eh_frame section
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.
> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> index 8cf19c5298fde..21f40805b417f 100644
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c
[ ... ]
> +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;
[ ... ]
> + 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);
[Severity: High]
Are we bypassing the pointer masking mitigation here?
The scoped_user_read_access_size() macro shadows the passed pointer (cie_ptr)
with a masked version to prevent speculative kernel accesses. However, the
memory read via UNSAFE_GET_USER_INC() uses the separate unmasked cur variable.
Could this allow speculative out-of-bounds reads to access kernel memory?
> + if (cie_id != EH_FRAME_CIE_ID)
> + return -EINVAL;
[ ... ]
> +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;
[ ... ]
> + 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);
[Severity: High]
Is this susceptible to the same pointer masking bypass as __read_cie()?
The unmasked cur variable is being used here instead of the masked fde_ptr.
> + 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;
> + }
[ ... ]
> +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_restore: {
> + u8 reg = DW_CFA_operand(opcode);
> + int idx;
> +
> + if (ctx->cie)
> + return -EINVAL;
> +
> + idx = reg_to_index(reg);
> + if (idx >= 0)
> + ctx->state.reg_rule[idx] = ctx->cie_state.reg_rule[idx];
> + break;
> + }
[Severity: High]
Does this properly restore the complete register state?
It appears only the rule type is restored from ctx->cie_state, but the
offset (reg_offset) and register number (reg_regnum) are not restored and
will remain at their previously modified values.
[ ... ]
> + case DW_CFA_advance_loc4: {
> + unsigned long offset;
> + u32 delta;
> +
> + UNSAFE_GET_USER_INC(delta, cur, end, Efault);
> + offset = delta * fde->cie.code_align;
[Severity: Medium]
Can this calculation overflow?
The multiplication of delta (a u32) and fde->cie.code_align (an unsigned int)
evaluates as a 32-bit unsigned integer before being assigned to the 64-bit
unsigned long offset.
If the product exceeds 32 bits, the high bits would be truncated, which might
cause the unwinder to desync from the actual PC.
> + ip += offset;
> + break;
> + }
[ ... ]
> + case DW_CFA_restore_extended: {
> + unsigned long reg;
> + int idx;
> +
> + if (ctx->cie)
> + return -EINVAL;
> +
> + ret = read_uleb128(&cur, end, ®);
> + if (ret)
> + return ret;
> +
> + idx = reg_to_index(reg);
> + if (idx >= 0)
> + ctx->state.reg_rule[idx] = ctx->cie_state.reg_rule[idx];
> + break;
> + }
[Severity: High]
This seems to have the same register state restoration issue as DW_CFA_restore.
Should reg_offset and reg_regnum also be restored here alongside the rule type?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821195259.2688377-1-jremus@linux.ibm.com?part=9
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 10/24] unwind_user/eh_frame: Detect .eh_frame_hdr sections in executables
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (8 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 09/24] unwind_user/eh_frame: Add support for reading .eh_frame section Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:10 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 11/24] unwind_user/eh_frame: Wire up unwind_user to eh_frame Jens Remus
` (13 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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] 48+ messages in thread* [RFC PATCH v2 11/24] unwind_user/eh_frame: Wire up unwind_user to eh_frame
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (9 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 10/24] unwind_user/eh_frame: Detect .eh_frame_hdr sections in executables Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:07 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 12/24] unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption Jens Remus
` (12 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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] 48+ messages in thread* Re: [RFC PATCH v2 11/24] unwind_user/eh_frame: Wire up unwind_user to eh_frame
2026-08-21 19:52 ` [RFC PATCH v2 11/24] unwind_user/eh_frame: Wire up unwind_user to eh_frame Jens Remus
@ 2026-08-21 20:07 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:07 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 1 potential issue(s) to consider:
- [Medium] Unadjusted instruction pointer passed to eh_frame_find() for return addresses.
--
--- Patch [11]: [RFC PATCH v2 11/24] unwind_user/eh_frame: Wire up unwind_user to eh_frame ---
commit 242304f470b193c92dc61308cce6cc9e66841ecf
Author: Jens Remus <jremus@linux.ibm.com>
unwind_user/eh_frame: Wire up unwind_user to eh_frame
This commit hooks up the eh_frame infrastructure to the unwind_user
interface. It enables the unwinder to use eh_frame data for user space
stack traces.
> 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: Medium]
Does this need to adjust state->ip for return addresses?
Since state->ip points to the instruction after the call, if a function
call is the final instruction of a block or function, passing the
unadjusted ip to eh_frame_find() might cause it to match the next
function's FDE instead of the caller's.
Would it be better to pass an adjusted instruction pointer (like
state->topmost ? state->ip : state->ip - 1) to ensure the correct unwind
rules are retrieved during normal user space unwinding?
> + return -ENOENT;
> + return unwind_user_next_common(state, &frame);
> +}
> +
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821195259.2688377-1-jremus@linux.ibm.com?part=11
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 12/24] unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (10 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 11/24] unwind_user/eh_frame: Wire up unwind_user to eh_frame Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:18 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 13/24] unwind_user/eh_frame: Show file name in debug output Jens Remus
` (11 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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 21f40805b417..327849ea4a69 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -1202,6 +1202,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] 48+ messages in thread* Re: [RFC PATCH v2 12/24] unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption
2026-08-21 19:52 ` [RFC PATCH v2 12/24] unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption Jens Remus
@ 2026-08-21 20:18 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:18 UTC (permalink / raw)
To: Jens Remus
Cc: Alexander Gordeev, Heiko Carstens, Vasily Gorbik,
linux-trace-kernel, linux-s390, Christian Borntraeger
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
New issues:
- [High] A Time-of-Check to Time-of-Use (TOCTOU) race condition allows the kernel to erroneously unregister a valid, newly loaded .eh_frame section due to address reuse.
- [High] The unwinder conflates actual memory corruption with kernel-imposed safety limits, permanently disabling unwinding for valid libraries.
Pre-existing issues:
- [Critical] Unchecked error from mas_erase() leads to freeing a maple tree section while it remains active in the tree, causing a Use-After-Free (UAF).
- [High] Lock drop and reacquire inside mas_erase() during mas_for_each() iteration causes a TOCTOU race and memory leak.
--
--- Patch [12]: [RFC PATCH v2 12/24] unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption ---
commit 14251b7471659003e9517d814ad704b284ed18df
Author: Jens Remus <jremus@linux.ibm.com>
unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption
This commit adds logic to unregister .eh_frame or .eh_frame_hdr sections on
demand when corruption is detected during lookup. This prevents repeated
attempts to parse bad unwind sections by removing them immediately upon an
error return.
> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> index 21f40805b417f..327849ea4a692 100644
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c
> @@ -1202,6 +1202,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");
> +
[Severity: High]
Does this introduce a race condition with memory unmapping and mapping?
If a thread is delayed after retrieving sec in eh_frame_find(), another
thread could unmap the library and map a new library at the exact same
virtual address, registering it with the same eh_frame_hdr_start. When the
first thread resumes and fails to parse against the new memory, passing the
virtual address sec->eh_frame_hdr_start to eh_frame_remove_section() seems
like it would unintentionally remove the newly loaded, valid library's
section.
[Severity: High]
Is it safe to assume -EINVAL always means corruption?
Looking at __do_cfi_program(), it also returns -EINVAL when kernel limits
are hit, such as exceeding EH_FRAME_CFI_INSN_LIMIT or
EH_FRAME_MAX_CODE_ALIGN.
If a valid library simply has a complex function that hits these limits,
wouldn't this code globally and permanently disable unwinding for it by
removing its section?
[Severity: High]
This is a pre-existing issue, but looking at eh_frame_remove_section()
which is called here, is there a memory leak if mas_erase() drops the lock?
eh_frame_remove_section() holds mtree_lock and iterates via mas_for_each().
It calls __eh_frame_remove_section(), which calls mas_erase().
If mas_erase() needs to allocate memory via mas_nomem(mas, GFP_KERNEL), it
can drop the tree lock. If a concurrent thread inserts a new section
covering the same range during this unlocked window, mas_erase() retries,
erases the new section, and returns it.
__eh_frame_remove_section() then sees the returned entry doesn't match sec,
returns -EINVAL, and skips call_srcu(), potentially leaking the new section.
[Severity: Critical]
This is a pre-existing issue, but looking at __eh_frame_remove_section()
called by eh_frame_remove_section(), can a memory allocation failure lead
to a use-after-free?
If mas_nomem() fails to allocate memory during mas_erase(), it sets an
error state and aborts without removing the node, but still returns the
original sec:
__eh_frame_remove_section() {
...
if (mas_erase(mas) != sec) {
return -EINVAL;
}
call_srcu(&eh_frame_srcu, &sec->rcu, eh_frame_free_srcu);
...
}
Because __eh_frame_remove_section() only checks if the return value does
not equal sec, it misses the error state completely. It then calls
call_srcu() to free the section while the node remains active in the maple
tree. Could subsequent calls to eh_frame_find() load and access this freed
memory?
> return ret;
> }
>
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821195259.2688377-1-jremus@linux.ibm.com?part=12
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 13/24] unwind_user/eh_frame: Show file name in debug output
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (11 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 12/24] unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:06 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 14/24] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option Jens Remus
` (10 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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 740316b19df3..70b4f9d12828 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 327849ea4a69..96237a96d263 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -1207,15 +1207,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);
}
@@ -1342,6 +1345,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;
@@ -1349,8 +1354,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;
}
@@ -1372,7 +1377,7 @@ static int __eh_frame_remove_section(struct ma_state *mas,
struct eh_frame_section *sec)
{
if (mas_erase(mas) != sec) {
- dbg("mas_erase failed: text=%lx\n", sec->text_start);
+ dbg_sec("mas_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] 48+ messages in thread* [RFC PATCH v2 14/24] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (12 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 13/24] unwind_user/eh_frame: Show file name in debug output Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:10 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 15/24] unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork Jens Remus
` (9 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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.
Changes in RFC v2:
- Fix compile error. (Sashiko AI)
- Correct loop counter type to prevent infinite loop. (Sashiko AI)
arch/Kconfig | 22 +++++++++
kernel/unwind/eh_frame.c | 90 ++++++++++++++++++++++++++++++++++
kernel/unwind/eh_frame_debug.h | 4 ++
3 files changed, 116 insertions(+)
diff --git a/arch/Kconfig b/arch/Kconfig
index ea969811f798..c06e5010a88a 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 entire .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 96237a96d263..b0be56e31508 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -1216,6 +1216,92 @@ 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 long i;
+
+ 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, &cur,
+ sec->binary_search_table_end,
+ table_enc, &func_addr);
+ if (ret) {
+ dbg_sec_ehfh(cur, "table[%lu]: failed to read function start address\n", i);
+ return ret;
+ }
+ if (i && func_addr <= prev_func_addr) {
+ dbg_sec(".eh_frame_hdr: table[%lu]: not sorted\n", i);
+ return -EINVAL;
+ }
+ prev_func_addr = func_addr;
+
+ /* Read FDE address from table */
+ ret = read_encoded_pointer(sec, &cur,
+ sec->binary_search_table_end,
+ table_enc, &fde_addr);
+ if (ret) {
+ dbg_sec_ehfh(cur, "table[%lu]: failed to read FDE pointer\n", i);
+ return ret;
+ }
+ if (fde_addr < sec->eh_frame_start) {
+ dbg_sec(".eh_frame_hdr: table[%lu]: invalid FDE address\n", i);
+ return -EINVAL;
+ }
+ }
+
+ ret = __read_fde(sec, fde_addr, &fde);
+ if (ret) {
+ dbg_sec(".eh_frame_hdr: table[%lu]: 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[%lu]: 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);
@@ -1351,6 +1437,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] 48+ messages in thread* Re: [RFC PATCH v2 14/24] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option
2026-08-21 19:52 ` [RFC PATCH v2 14/24] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option Jens Remus
@ 2026-08-21 20:10 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:10 UTC (permalink / raw)
To: Jens Remus
Cc: Christian Borntraeger, linux-trace-kernel, Alexander Gordeev,
linux-s390, Heiko Carstens, Vasily Gorbik
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Calling printk (via dbg_sec and dbg_sec_ehfh) while user access (UACCESS) is enabled.
- [High] Missing cond_resched() in massive, unbounded .eh_frame validation loop.
- [High] Misuse of scoped_user_read_access_size completely bypasses pointer masking mitigations.
--
--- Patch [14]: [RFC PATCH v2 14/24] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option ---
commit e134b713f5753b4c2aa710bb7ce4a784930e6ccd
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/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> index 96237a96d263e..b0be56e315080 100644
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c
[ ... ]
> @@ -1216,6 +1216,92 @@ 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)
> +{
[ ... ]
> + 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]
Could this unbounded loop cause CPU stalls or trigger soft lockup warnings?
Since fde_count is controlled by the size of the mapped .eh_frame_hdr VMA,
it can be extremely large for maliciously crafted ELF files, potentially
stalling the CPU for seconds or minutes.
Because mmap_read_lock is released before this is called, would it be safer
to add a cond_resched() in this loop?
> + 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, &cur,
> + sec->binary_search_table_end,
> + table_enc, &func_addr);
[Severity: High]
Does this bypass the Spectre v1 mitigations provided by the scoped access
macro?
The scoped_user_read_access_size() macro shadows the table_start_ptr variable
to enforce usage of a masked pointer. However, the code ignores that masked
pointer and instead uses the unmasked cur pointer derived outside the scope:
cur = sec->binary_search_table_start + i * entry_size;
When read_encoded_pointer() uses this unmasked pointer inside
unsafe_get_user(), the masking mitigations are bypassed, allowing potential
arbitrary speculative reads.
> + if (ret) {
> + dbg_sec_ehfh(cur, "table[%lu]: failed to read function start address\n", i);
[Severity: High]
Is it safe to call printk wrappers like dbg_sec_ehfh() or dbg_sec() while user
access (UACCESS) is enabled?
Calling complex functions like printk inside a scoped_user_read_access_size()
block can lead to deadlocks or kernel panics. It does internal locking, can
sleep, and may clobber architecture-specific user access state like the SMAP
AC flag.
> + return ret;
> + }
> + if (i && func_addr <= prev_func_addr) {
> + dbg_sec(".eh_frame_hdr: table[%lu]: not sorted\n", i);
> + return -EINVAL;
> + }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821195259.2688377-1-jremus@linux.ibm.com?part=14
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 15/24] unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (13 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 14/24] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:09 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 16/24] unwind_user/eh_frame: Ignore DW_CFA_GNU_args_size Jens Remus
` (8 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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 70b4f9d12828..3c88ab2f7858 100644
--- a/include/linux/eh_frame.h
+++ b/include/linux/eh_frame.h
@@ -32,6 +32,7 @@ struct eh_frame_section {
#define EH_FRAME_MT_FLAGS (MT_FLAGS_USE_RCU)
#define INIT_MM_EH_FRAME .eh_frame_mt = MTREE_INIT(eh_frame_mt, EH_FRAME_MT_FLAGS),
+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,
@@ -51,6 +52,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 b0be56e31508..1ac7308814a1 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -1501,6 +1501,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] 48+ messages in thread* Re: [RFC PATCH v2 15/24] unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork
2026-08-21 19:52 ` [RFC PATCH v2 15/24] unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork Jens Remus
@ 2026-08-21 20:09 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:09 UTC (permalink / raw)
To: Jens Remus
Cc: linux-s390, Heiko Carstens, Alexander Gordeev,
Christian Borntraeger, linux-trace-kernel, Vasily Gorbik
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Blindly duplicating all eh_frame sections ignores VMA inheritance rules (e.g., VM_DONTCOPY), leading to stale unwinding data.
- [Medium] Mixing goto-based error handling with guard() cleanup in the same function.
--
--- Patch [15]: [RFC PATCH v2 15/24] unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork ---
commit cedc4c84b3351e03d89a9e7fc338f35818891c5a
Author: Jens Remus <jremus@linux.ibm.com>
unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork
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.
> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> index b0be56e315080..1ac7308814a16 100644
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c
[ ... ]
> @@ -1501,6 +1501,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)
> +{
[ ... ]
> +}
> +
> +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);
[Severity: Medium]
Is it acceptable to mix goto-based error handling with guard() in the same
function? The cleanup guidelines suggest converting all resources to
scope-based cleanup or none to avoid confusing ownership semantics.
> +
> + mt_for_each(&oldmm->eh_frame_mt, oldsec, index, ULONG_MAX) {
[Severity: High]
Does this unconditionally duplicate all eh_frame sections from the parent
without checking if the underlying VMA was actually copied during the fork?
If a process sets a region to VM_DONTCOPY or VM_WIPEONFORK before forking,
the child won't inherit the VMA, but it appears it will still inherit
the eh_frame section here. If the child process later maps new executable
code at the same address, could the unwinder use the parent's stale rules?
> + 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;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821195259.2688377-1-jremus@linux.ibm.com?part=15
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 16/24] unwind_user/eh_frame: Ignore DW_CFA_GNU_args_size
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (14 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 15/24] unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:03 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 17/24] unwind_user/eh_frame: Add support for DWARF expressions Jens Remus
` (7 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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 1ac7308814a1..d185187b0c34 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -980,6 +980,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] 48+ messages in thread* [RFC PATCH v2 17/24] unwind_user/eh_frame: Add support for DWARF expressions
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (15 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 16/24] unwind_user/eh_frame: Ignore DW_CFA_GNU_args_size Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:18 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 18/24] x86/uaccess: Add unsafe_copy_from_user() implementation Jens Remus
` (6 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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>
---
Notes (jremus):
Changes in RFC v2:
- Fix expression length overflow checks. (Sashiko AI)
include/asm-generic/unwind_user_eh_frame.h | 41 ++++++++++++
include/linux/unwind_user_eh_frame_types.h | 37 +++++++++++
kernel/unwind/eh_frame.c | 72 +++++++++++-----------
3 files changed, 114 insertions(+), 36 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 da238557605b..a3cd3b3b55c7 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
@@ -14,6 +16,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 1
#endif
@@ -47,5 +53,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 d185187b0c34..a98804dbe588 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];
@@ -803,6 +772,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 < cur || 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;
@@ -970,12 +960,22 @@ static __always_inline int __do_cfi_insn(struct eh_frame_section *sec,
if (ret)
return ret;
- if (cur + expr_len > end)
+ if (cur + expr_len < cur || 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] 48+ messages in thread* Re: [RFC PATCH v2 17/24] unwind_user/eh_frame: Add support for DWARF expressions
2026-08-21 19:52 ` [RFC PATCH v2 17/24] unwind_user/eh_frame: Add support for DWARF expressions Jens Remus
@ 2026-08-21 20:18 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:18 UTC (permalink / raw)
To: Jens Remus
Cc: linux-s390, Alexander Gordeev, Christian Borntraeger,
Heiko Carstens, linux-trace-kernel, Vasily Gorbik
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Unaligned stack buffer passed to unsafe_copy_from_user() causes undefined behavior
--
--- Patch [17]: [RFC PATCH v2 17/24] unwind_user/eh_frame: Add support for DWARF expressions ---
commit 38e0b8657cdb2bd1e95dd3bc74b1aa71e0063c9a
Author: Jens Remus <jremus@linux.ibm.com>
unwind_user/eh_frame: Add support for DWARF expressions
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.
> diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
> index d185187b0c34e..a98804dbe5886 100644
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c
> @@ -803,6 +772,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 < cur || cur + expr_len > end)
> + return -EINVAL;
> +
> + if (expr_len > sizeof(expr))
> + return -EOPNOTSUPP;
> + unsafe_copy_from_user(&expr, (void __user *)cur, expr_len, Efault);
[Severity: Low]
Does this unaligned stack buffer passed to unsafe_copy_from_user() cause
undefined behavior?
Passing a 1-byte aligned char array to unsafe_copy_from_user() violates C
alignment rules. In architecture-specific implementations, the macro
explicitly casts the destination pointer to larger integer types (e.g.,
u64 *) to perform optimized word-sized memory assignments. Dereferencing
an unaligned pointer is Undefined Behavior in C.
While x86 hardware gracefully handles unaligned memory accesses, the
compiler's optimizer is legally allowed to assume u64 * is 8-byte aligned
and could emit trapping instructions. Furthermore, if this generic .eh_frame
code is later enabled on architectures with strict hardware alignment
enforcement, it will immediately trigger alignment exceptions.
Could this buffer be declared with __aligned(8) or as an array of u64?
> + ret = eh_frame_do_def_cfa_expression(expr, expr_len, target_ip, &ctx->state);
> + if (ret)
> + return ret;
> + cur += expr_len;
> + break;
> + }
> +
[ ... ]
> @@ -970,12 +960,22 @@ static __always_inline int __do_cfi_insn(struct eh_frame_section *sec,
> if (ret)
> return ret;
>
> - if (cur + expr_len > end)
> + if (cur + expr_len < cur || 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: Low]
Is this the same alignment issue as above?
Could this buffer also be declared with __aligned(8) or as an array of u64?
> + 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;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821195259.2688377-1-jremus@linux.ibm.com?part=17
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 18/24] x86/uaccess: Add unsafe_copy_from_user() implementation
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (16 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 17/24] unwind_user/eh_frame: Add support for DWARF expressions Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:11 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 19/24] unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86 Jens Remus
` (5 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song,
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] 48+ messages in thread* Re: [RFC PATCH v2 18/24] x86/uaccess: Add unsafe_copy_from_user() implementation
2026-08-21 19:52 ` [RFC PATCH v2 18/24] x86/uaccess: Add unsafe_copy_from_user() implementation Jens Remus
@ 2026-08-21 20:11 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:11 UTC (permalink / raw)
To: Jens Remus
Cc: linux-s390, Christian Borntraeger, Heiko Carstens,
linux-trace-kernel, Alexander Gordeev, Vasily Gorbik
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Macro variable shadowing risk in `unsafe_copy_to_user` and `unsafe_copy_from_user`.
--
--- Patch [18]: [RFC PATCH v2 18/24] x86/uaccess: Add unsafe_copy_from_user() implementation ---
commit 6d0ae2cda144449266a35cb0d45ee49364b75b97
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() macro. It also updates the existing
unsafe_copy_to_user() macro to use shorter internal variable names.
> diff --git a/arch/x86/include/asm/uaccess.h b/arch/x86/include/asm/uaccess.h
> index 3a0dd3c2b233e..235886106f31e 100644
> --- 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 this macro change cause dangerous variable shadowing?
By renaming the internal variables from __ucu_dst to generic names like __dst,
__src, and __len, if a caller passes arguments with those exact names, the
macro will expand to something like void __user *__dst = (__dst);. This
results in self-initialization using uninitialized stack memory, which could
lead to arbitrary memory reads or writes when used with SMAP disabled.
> + 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); \
[Severity: Medium]
Does this new macro suffer from the same variable shadowing risk?
Using generic internal names like __dst, __src, and __len might cause
accidental self-initialization if a caller happens to use the same variable
names for the arguments. Should we use more uniquely prefixed names here
similar to the old __ucu_dst?
> + 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/20260821195259.2688377-1-jremus@linux.ibm.com?part=18
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 19/24] unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (17 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 18/24] x86/uaccess: Add unsafe_copy_from_user() implementation Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:16 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 20/24] unwind_user/eh_frame/x86: Handle PLT expressions Jens Remus
` (4 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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] 48+ messages in thread* Re: [RFC PATCH v2 19/24] unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86
2026-08-21 19:52 ` [RFC PATCH v2 19/24] unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86 Jens Remus
@ 2026-08-21 20:16 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:16 UTC (permalink / raw)
To: Jens Remus
Cc: linux-s390, Heiko Carstens, Alexander Gordeev, Vasily Gorbik,
Christian Borntraeger, linux-trace-kernel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Enabling `.eh_frame` unwinding on x86_64 breaks user stack traces for 32-bit compat binaries due to incorrect error handling in the generic unwind loop.
--
--- Patch [19]: [RFC PATCH v2 19/24] unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86 ---
commit 26326eedb731dafef6946d5d887c188233f39043
Author: Jens Remus <jremus@linux.ibm.com>
unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86
This patch enables .eh_frame user stack unwinding for x86-64 by selecting
HAVE_UNWIND_USER_EH_FRAME and providing the necessary register accessors.
> diff --git a/arch/x86/include/asm/unwind_user.h b/arch/x86/include/asm/unwind_user.h
> index c96645c824d1b..df1966bcb0f6e 100644
> --- a/arch/x86/include/asm/unwind_user.h
> +++ b/arch/x86/include/asm/unwind_user.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;
[Severity: High]
Does returning -EINVAL here break the stack trace fallback mechanism for
32-bit compat tasks?
In kernel/unwind/user.c:unwind_user_next(), the generic unwinder only
continues to the next method (like the frame pointer unwinder) if it receives
-ENOENT:
kernel/unwind/user.c:unwind_user_next() {
...
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;
...
}
By returning -EINVAL for 32-bit tasks, the unwinder hits the default case and
aborts entirely by setting state->done = true. This truncates the user stack
trace instead of safely falling back to the FP unwinder.
Would returning -ENOENT be more appropriate here when the current task is
not 64-bit to ensure the fallback pipeline continues correctly?
> +
> + switch (regnum) {
> + /* DWARF register numbers 0..15 */
> + case 0: *val = regs->ax; break;
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821195259.2688377-1-jremus@linux.ibm.com?part=19
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 20/24] unwind_user/eh_frame/x86: Handle PLT expressions
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (18 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 19/24] unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86 Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:17 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 21/24] unwind_user/eh_frame/x86: Handle DRAP expressions Jens Remus
` (3 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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 a98804dbe588..0a81ba1a0377 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] 48+ messages in thread* Re: [RFC PATCH v2 20/24] unwind_user/eh_frame/x86: Handle PLT expressions
2026-08-21 19:52 ` [RFC PATCH v2 20/24] unwind_user/eh_frame/x86: Handle PLT expressions Jens Remus
@ 2026-08-21 20:17 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:17 UTC (permalink / raw)
To: Jens Remus
Cc: linux-trace-kernel, Heiko Carstens, Vasily Gorbik, linux-s390,
Alexander Gordeev, Christian Borntraeger
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The patch claims to support i386 PLT unwinding but fundamentally breaks it by hardcoding x86-64 DWARF register macros.
--
--- Patch [20]: [RFC PATCH v2 20/24] unwind_user/eh_frame/x86: Handle PLT expressions ---
commit 75d2af7b6df62f61cc44905f96f37d91b80e466a
Author: Jens Remus <jremus@linux.ibm.com>
unwind_user/eh_frame/x86: Handle PLT expressions
This commit adds x86-specific support for handling the CFA expression
found in GNU linker generated .eh_frame for PLT. It intends to enable
unwinding through GNU linker generated PLT entries on both i386 and
x86-64.
> 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
[ ... ]
> @@ -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 eh_frame_do_def_cfa_expression(const char *expr,
> + int size,
> + unsigned long ip,
> + struct eh_frame_reg_state *reg_state)
> +{
[ ... ]
> + 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;
[Severity: Medium]
The commit message mentions enabling unwinding for both i386 and x86-64,
but does this check inadvertently break i386 support?
EH_FRAME_REG_SP and EH_FRAME_REG_RA are defined under CONFIG_X86_64 and
correspond to x86-64 DWARF register numbers (7 and 16). For i386, the DWARF
register numbers are different (ESP is 4, EIP is 8).
When unwinding an i386 PLT expression, such as for a 32-bit compat process
running under a 64-bit kernel, will this check unconditionally reject the
expression and return -EOPNOTSUPP because 4 does not equal 7?
Also, if this code is compiled natively for 32-bit, would this cause a build
failure because the EH_FRAME_REG_SP and EH_FRAME_REG_RA macros are not
defined outside of the CONFIG_X86_64 block?
> +
> + /* 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;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821195259.2688377-1-jremus@linux.ibm.com?part=20
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 21/24] unwind_user/eh_frame/x86: Handle DRAP expressions
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (19 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 20/24] unwind_user/eh_frame/x86: Handle PLT expressions Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:15 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 22/24] s390/ptrace: Provide frame_pointer() Jens Remus
` (2 subsequent siblings)
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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 0a81ba1a0377..d1656f5d40f1 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -1098,18 +1098,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 ||
@@ -1150,6 +1165,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;
}
@@ -1184,6 +1209,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] 48+ messages in thread* Re: [RFC PATCH v2 21/24] unwind_user/eh_frame/x86: Handle DRAP expressions
2026-08-21 19:52 ` [RFC PATCH v2 21/24] unwind_user/eh_frame/x86: Handle DRAP expressions Jens Remus
@ 2026-08-21 20:15 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:15 UTC (permalink / raw)
To: Jens Remus
Cc: Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
Christian Borntraeger, linux-trace-kernel, linux-s390
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The newly added DRAP DWARF expression handling for the Frame Pointer (FP) uses a generic register rule that unconditionally fails for non-topmost frames, causing premature stack trace truncation.
--
--- Patch [21]: [RFC PATCH v2 21/24] unwind_user/eh_frame/x86: Handle DRAP expressions ---
commit a27cd1aa620d26189650492016f14beeb35a7207
Author: Jens Remus <jremus@linux.ibm.com>
unwind_user/eh_frame/x86: Handle DRAP expressions
This commit adds x86-specific handling for DRAP CFA and FP expressions.
It supports unwinding through frames with dynamically realigned stacks by
parsing the corresponding DWARF expressions.
> 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
[ ... ]
> @@ -1184,6 +1209,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;
[Severity: Medium]
Will this generic register rule cause stack trace truncation for non-topmost
frames?
Looking at unwind_user_next_common(), UNWIND_USER_RULE_REG_OFFSET_DEREF is
unconditionally rejected unless it's the topmost frame:
kernel/unwind/user.c:unwind_user_next_common() {
...
case UNWIND_USER_RULE_REG_OFFSET_DEREF:
if (!state->topmost || unwind_user_get_reg(&fp, frame->fp.regnum))
return -EINVAL;
...
}
Because EH_FRAME_REG_FP is tracked across frames, could this use a dedicated
FP rule to avoid aborting the stack trace when encountering a DRAP frame that
isn't the innermost frame?
> default:
> return -EINVAL;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821195259.2688377-1-jremus@linux.ibm.com?part=21
^ permalink raw reply [flat|nested] 48+ messages in thread
* [RFC PATCH v2 22/24] s390/ptrace: Provide frame_pointer()
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (20 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 21/24] unwind_user/eh_frame/x86: Handle DRAP expressions Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:07 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 23/24] unwind_user/eh_frame/s390: Enable eh_frame unwinding on s390 Jens Remus
2026-08-21 19:52 ` [RFC PATCH v2 24/24] unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections Jens Remus
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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] 48+ messages in thread* [RFC PATCH v2 23/24] unwind_user/eh_frame/s390: Enable eh_frame unwinding on s390
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (21 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 22/24] s390/ptrace: Provide frame_pointer() Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:15 ` sashiko-bot
2026-08-21 19:52 ` [RFC PATCH v2 24/24] unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections Jens Remus
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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 = ¤t->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] 48+ messages in thread* [RFC PATCH v2 24/24] unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections
2026-08-21 19:52 [RFC PATCH v2 00/24] unwind_user: Implement .eh_frame handling Jens Remus
` (22 preceding siblings ...)
2026-08-21 19:52 ` [RFC PATCH v2 23/24] unwind_user/eh_frame/s390: Enable eh_frame unwinding on s390 Jens Remus
@ 2026-08-21 19:52 ` Jens Remus
2026-08-21 20:22 ` sashiko-bot
23 siblings, 1 reply; 48+ messages in thread
From: Jens Remus @ 2026-08-21 19:52 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, Fangrui Song
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>
---
Notes (jremus):
Changes in RFC v2:
- eh_frame_{register|unregister}(): Reject zero user_data or size.
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 3c88ab2f7858..3a61ed0ae739 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 {
@@ -40,6 +42,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)
@@ -78,6 +86,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 d1656f5d40f1..063133d1bf97 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"
@@ -1609,3 +1610,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] 48+ messages in thread* Re: [RFC PATCH v2 24/24] unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections
2026-08-21 19:52 ` [RFC PATCH v2 24/24] unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections Jens Remus
@ 2026-08-21 20:22 ` sashiko-bot
0 siblings, 0 replies; 48+ messages in thread
From: sashiko-bot @ 2026-08-21 20:22 UTC (permalink / raw)
To: Jens Remus
Cc: linux-s390, linux-trace-kernel, Vasily Gorbik, Alexander Gordeev,
Christian Borntraeger, Heiko Carstens
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
New issues:
- [High] The strict `size != sizeof(data)` check completely breaks UAPI backward and forward compatibility for extensible structs.
- [Medium] Silent truncation of the 64-bit `eh_frame_hdr_start` address to a 32-bit `unsigned long` during unregistration on 32-bit architectures.
Pre-existing issues:
- [High] If `mas_erase` fails due to an out-of-memory condition, it returns the target entry but leaves it in the maple tree. `__eh_frame_remove_section` fails to check `mas_is_err(mas)`, freeing the entry while it remains accessible in the tree.
- [High] The unregister prctl triggers an unbounded iteration over the maple tree while holding a spinlock, allowing an unprivileged user to cause a CPU soft lockup.
--
--- Patch [24]: [RFC PATCH v2 24/24] unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections ---
commit 51adfd3b1e2795e1bc35c760119a8842b4e76983
Author: Jens Remus <jremus@linux.ibm.com>
unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections
This commit adds prctl interfaces to register and unregister .eh_frame_hdr
sections. It allows dynamic linkers to notify the kernel about the location
of these sections using a new eh_frame_setup structure.
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
> --- a/kernel/unwind/eh_frame.c
> +++ b/kernel/unwind/eh_frame.c
[ ... ]
> @@ -1609,3 +1610,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: High]
Does this strict size check prevent future UAPI extensions? Since this
is a new UAPI structure, rejecting sizes that do not exactly match the
kernel's structure size prevents extending the structure later without
breaking backward compatibility. Would it be better to use
copy_struct_from_user() here to handle size mismatches by verifying
trailing zeroes?
> +
> + 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;
[Severity: High]
Is this strict size check similarly going to cause extensibility issues
for the unregister path? Like the register function, could this use
copy_struct_from_user() to safely allow future modifications?
> +
> + 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]
Will data.eh_frame_hdr_start be silently truncated on 32-bit architectures?
The user provides a 64-bit __u64 address, but eh_frame_remove_section()
expects an unsigned long. If the upper 32 bits are set by userspace on a
32-bit architecture, this could truncate into a valid 32-bit address without
any bounds validation.
[Severity: High]
This isn't a bug introduced by this patch, but does exposing this function
to userspace via prctl allow an unprivileged user to cause a CPU soft lockup?
Looking at eh_frame_remove_section(), it performs an unbounded iteration over
the maple tree while holding a spinlock:
kernel/unwind/eh_frame.c:eh_frame_remove_section() {
...
mtree_lock(&mm->eh_frame_mt);
mas_for_each(&mas, sec, ULONG_MAX) {
if (sec->eh_frame_hdr_start == eh_frame_hdr_start) {
found = true;
ret |= __eh_frame_remove_section(&mas, sec);
}
}
mtree_unlock(&mm->eh_frame_mt);
...
}
Could an attacker register a massive number of non-overlapping sections and
then trigger a long unbounded loop with preemption disabled?
[Severity: High]
This is also a pre-existing issue, but the new prctl makes it trivially
reachable by unprivileged userspace, which could lead to a use-after-free.
When __eh_frame_remove_section() attempts to remove a section:
kernel/unwind/eh_frame.c:__eh_frame_remove_section() {
...
if (mas_erase(mas) != sec) {
dbg_sec("mas_erase failed: text=%lx\n", sec->text_start);
return -EINVAL;
}
call_srcu(&eh_frame_srcu, &sec->rcu, eh_frame_free_srcu);
...
}
If mas_erase() fails due to an out-of-memory condition, it can set an error
state and return the target entry without removing it from the tree. Because
the code does not check mas_is_err(mas), it frees the entry while it remains
accessible in the maple tree. Should this verify mas_is_err() before freeing
the section?
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821195259.2688377-1-jremus@linux.ibm.com?part=24
^ permalink raw reply [flat|nested] 48+ messages in thread