All of lore.kernel.org
 help / color / mirror / Atom feed
From: Jens Remus <jremus@linux.ibm.com>
To: linux-kernel@vger.kernel.org, linux-trace-kernel@vger.kernel.org,
	linux-s390@vger.kernel.org, x86@kernel.org,
	Steven Rostedt <rostedt@kernel.org>,
	Josh Poimboeuf <jpoimboe@kernel.org>,
	Peter Zijlstra <peterz@infradead.org>,
	Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Jens Remus <jremus@linux.ibm.com>,
	Heiko Carstens <hca@linux.ibm.com>,
	Vasily Gorbik <gor@linux.ibm.com>,
	Alexander Gordeev <agordeev@linux.ibm.com>,
	Ilya Leoshkevich <iii@linux.ibm.com>,
	Indu Bhagat <ibhagatgnu@gmail.com>,
	Thomas Gleixner <tglx@kernel.org>, Ingo Molnar <mingo@redhat.com>,
	Borislav Petkov <bp@alien8.de>,
	Dave Hansen <dave.hansen@linux.intel.com>,
	"H. Peter Anvin" <hpa@zytor.com>,
	Namhyung Kim <namhyung@kernel.org>,
	Andrii Nakryiko <andrii@kernel.org>, Kees Cook <kees@kernel.org>,
	Sam James <sam@gentoo.org>
Subject: [RFC PATCH v1 16/25] unwind_user/eh_frame: Add linear .eh_frame search fallback
Date: Tue, 18 Aug 2026 16:49:45 +0200	[thread overview]
Message-ID: <20260818144954.2320378-17-jremus@linux.ibm.com> (raw)
In-Reply-To: <20260818144954.2320378-1-jremus@linux.ibm.com>

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

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

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

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

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

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

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

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


  parent reply	other threads:[~2026-08-18 14:50 UTC|newest]

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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260818144954.2320378-17-jremus@linux.ibm.com \
    --to=jremus@linux.ibm.com \
    --cc=agordeev@linux.ibm.com \
    --cc=andrii@kernel.org \
    --cc=bp@alien8.de \
    --cc=dave.hansen@linux.intel.com \
    --cc=gor@linux.ibm.com \
    --cc=hca@linux.ibm.com \
    --cc=hpa@zytor.com \
    --cc=ibhagatgnu@gmail.com \
    --cc=iii@linux.ibm.com \
    --cc=jpoimboe@kernel.org \
    --cc=kees@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-s390@vger.kernel.org \
    --cc=linux-trace-kernel@vger.kernel.org \
    --cc=mathieu.desnoyers@efficios.com \
    --cc=mingo@redhat.com \
    --cc=namhyung@kernel.org \
    --cc=peterz@infradead.org \
    --cc=rostedt@kernel.org \
    --cc=sam@gentoo.org \
    --cc=tglx@kernel.org \
    --cc=x86@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.