[RFC PATCH v2 14/24] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option
From: Jens Remus
Date: Fri Aug 21 2026 - 16:02:02 EST
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@xxxxxxxxxxxxx>
---
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