[RFC PATCH v2 17/24] unwind_user/eh_frame: Add support for DWARF expressions

From: Jens Remus

Date: Fri Aug 21 2026 - 15:55:48 EST


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

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