[PATCH v7 06/11] arm64/sframe: Validate IP addresses

From: Dylan Hatch

Date: Fri Sep 18 2026 - 18:44:43 EST


Validate the given IP and computed function start address against the
known vmlinux and module text address ranges.

This requires arch-specific validation for vmlinux. This is because
arm64 keeps .exit.text (normally discarded) and .rodata.text, both of
both of which lie outside the bounds of .text and .init.text. Note that
.rodata.text contains code that is never executed by the kernel mapping,
but for which the toolchain nonetheless generates sframe data, and needs
to be considered valid at lookup time.

Suggested-by: Jens Remus <jremus@xxxxxxxxxxxxx>
Signed-off-by: Dylan Hatch <dylanbhatch@xxxxxxxxxx>

---

This patch is split out from v6 patch "sframe: Introduce in-kernel
SFRAME_VALIDATION", and includes just the IP validation logic without
the SFRAME_VALIDATION option.
---
arch/arm64/include/asm/sections.h | 1 +
arch/arm64/include/asm/unwind_sframe.h | 32 +++++++++++++++++++
arch/arm64/kernel/vmlinux.lds.S | 2 ++
kernel/unwind/sframe.c | 43 +++++++++++++++++++++++++-
4 files changed, 77 insertions(+), 1 deletion(-)
create mode 100644 arch/arm64/include/asm/unwind_sframe.h

diff --git a/arch/arm64/include/asm/sections.h b/arch/arm64/include/asm/sections.h
index 51b0d594239eb..5edb4304f661e 100644
--- a/arch/arm64/include/asm/sections.h
+++ b/arch/arm64/include/asm/sections.h
@@ -23,6 +23,7 @@ extern char __irqentry_text_start[], __irqentry_text_end[];
extern char __mmuoff_data_start[], __mmuoff_data_end[];
extern char __entry_tramp_text_start[], __entry_tramp_text_end[];
extern char __relocate_new_kernel_start[], __relocate_new_kernel_end[];
+extern char _srodatatext[], _erodatatext[];

static inline size_t entry_tramp_text_size(void)
{
diff --git a/arch/arm64/include/asm/unwind_sframe.h b/arch/arm64/include/asm/unwind_sframe.h
new file mode 100644
index 0000000000000..8eb720f59434c
--- /dev/null
+++ b/arch/arm64/include/asm/unwind_sframe.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _ASM_ARM64_UNWIND_SFRAME_H
+#define _ASM_ARM64_UNWIND_SFRAME_H
+
+#include <linux/module.h>
+#include <linux/sframe.h>
+#include <asm/sections.h>
+
+static inline bool sframe_is_kernel_ip_valid(unsigned long ip)
+{
+ if (is_kernel_text(ip) || is_kernel_inittext(ip))
+ return true;
+
+ /* .exit.text is retained in vmlinux on arm64. */
+ if (ip >= (unsigned long)__exittext_begin &&
+ ip < (unsigned long)__exittext_end)
+ return true;
+
+ /*
+ * .rodata.text is never executed from the kernel mapping, but
+ * still has sframe data
+ */
+ if (ip >= (unsigned long)_srodatatext &&
+ ip < (unsigned long)_erodatatext)
+ return true;
+
+ return false;
+}
+
+#define sframe_is_kernel_ip_valid sframe_is_kernel_ip_valid
+
+#endif /* _ASM_ARM64_UNWIND_SFRAME_H */
diff --git a/arch/arm64/kernel/vmlinux.lds.S b/arch/arm64/kernel/vmlinux.lds.S
index eb1f54829503c..82fd20ccb7c43 100644
--- a/arch/arm64/kernel/vmlinux.lds.S
+++ b/arch/arm64/kernel/vmlinux.lds.S
@@ -237,12 +237,14 @@ SECTIONS

/* code sections that are never executed via the kernel mapping */
.rodata.text : {
+ _srodatatext = .;
TRAMP_TEXT
HIBERNATE_TEXT
KEXEC_TEXT
IDMAP_TEXT
. = ALIGN(PAGE_SIZE);
}
+ _erodatatext = .;

idmap_pg_dir = .;
. += PAGE_SIZE;
diff --git a/kernel/unwind/sframe.c b/kernel/unwind/sframe.c
index 503d4a2beb50e..e6542bb678585 100644
--- a/kernel/unwind/sframe.c
+++ b/kernel/unwind/sframe.c
@@ -42,6 +42,45 @@ struct sframe_fre_internal {
unsigned char dw_size;
};

+#ifndef sframe_is_kernel_ip_valid
+
+static __always_inline bool sframe_is_kernel_ip_valid(unsigned long ip)
+{
+ return is_kernel_text(ip) || is_kernel_inittext(ip);
+}
+
+#endif
+
+#ifdef CONFIG_MODULES
+
+static __always_inline bool sframe_is_sec_module_ip_valid(struct sframe_section *sec,
+ unsigned long ip)
+{
+ struct module *mod = container_of(sec, struct module, arch.sframe_sec);
+
+ return within_module_mem_type(ip, mod, MOD_TEXT) ||
+ within_module_mem_type(ip, mod, MOD_INIT_TEXT);
+}
+
+#else
+
+static __always_inline bool sframe_is_sec_module_ip_valid(struct sframe_section *sec,
+ unsigned long ip)
+{
+ return false;
+}
+
+#endif
+
+static __always_inline bool is_sec_ip_valid(struct sframe_section *sec,
+ unsigned long ip)
+{
+ if (sec == &kernel_sfsec)
+ return sframe_is_kernel_ip_valid(ip);
+
+ return sframe_is_sec_module_ip_valid(sec, ip);
+}
+
static __always_inline unsigned char fre_type_to_size(unsigned char fre_type)
{
if (fre_type > 2)
@@ -68,6 +107,8 @@ static __always_inline int __read_fde(struct sframe_section *sec,
_fde = (struct sframe_fde_v3 *)fde_addr;

func_addr = fde_addr + _fde->func_start_off;
+ if (!is_sec_ip_valid(sec, func_addr))
+ return -EINVAL;

fda_addr = sec->fres_start + _fde->fres_off;
if (fda_addr + sizeof(struct sframe_fda_v3) > sec->fres_end ||
@@ -438,7 +479,7 @@ int sframe_find(unsigned long ip, struct unwind_frame *frame)
if (!frame)
return -EINVAL;

- if (is_kernel_text(ip) || is_kernel_inittext(ip)) {
+ if (sframe_is_kernel_ip_valid(ip)) {
if (!sframe_init)
return -EINVAL;

--
2.55.0.1082.g2b9226bbc0-goog