public inbox for linux-modules@vger.kernel.org
 help / color / mirror / Atom feed
* Re: [PATCH 0/2] kallsyms: show typed function parameters in oops/WARN dumps
From: Alexey Dobriyan @ 2026-03-23 18:43 UTC (permalink / raw)
  To: Sasha Levin
  Cc: Andrew Morton, Masahiro Yamada, Nathan Chancellor, Nicolas Schier,
	Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen,
	H. Peter Anvin, Peter Zijlstra, Josh Poimboeuf, Petr Mladek,
	Alexei Starovoitov, Jonathan Corbet, David Gow, Kees Cook,
	Greg KH, Luis Chamberlain, Steven Rostedt, Helge Deller,
	Randy Dunlap, Geert Uytterhoeven, Juergen Gross, James Bottomley,
	Vlastimil Babka, Laurent Pinchart, Petr Pavlu, x86, linux-kernel,
	linux-kbuild, linux-doc, linux-modules, bpf
In-Reply-To: <20260323164858.1939248-1-sashal@kernel.org>

On Mon, Mar 23, 2026 at 12:48:55PM -0400, Sasha Levin wrote:
>  Function parameters (paraminfo_demo_crash):
>   uts      (struct new_utsname *) = 0xffffffffb8ca8d00
>    .sysname = "Linux"                        .nodename = "localhost"
>    .release = "7.0.0-rc2-00006-g3190..."     .version = "#45 SMP PRE"
>   file     (struct file *       ) = 0xffffa0a3c250acc0
>    .f_mode = (fmode_t)67993630               .f_op = (struct file_operations *)0xffffffffb7237620
>    .f_flags = (unsigned int)32769            .f_cred = (struct cred *)0xffffa0a3c2e06a80
>    .dentry = (struct dentry *)0xffffa0a3c0978cc0

Should this be in crash's format?

	struct dentry ffffffffffff0000

^ permalink raw reply

* [PATCH 2/2] kallsyms: add BTF-based deep parameter rendering in oops dumps
From: Sasha Levin @ 2026-03-23 16:48 UTC (permalink / raw)
  To: Andrew Morton, Masahiro Yamada, Nathan Chancellor, Nicolas Schier
  Cc: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen,
	H. Peter Anvin, Peter Zijlstra, Josh Poimboeuf, Petr Mladek,
	Alexei Starovoitov, Jonathan Corbet, David Gow, Kees Cook,
	Greg KH, Luis Chamberlain, Steven Rostedt, Helge Deller,
	Randy Dunlap, Geert Uytterhoeven, Juergen Gross, James Bottomley,
	Alexey Dobriyan, Vlastimil Babka, Laurent Pinchart, Petr Pavlu,
	x86, linux-kernel, linux-kbuild, linux-doc, linux-modules, bpf,
	Sasha Levin
In-Reply-To: <20260323164858.1939248-1-sashal@kernel.org>

When CONFIG_KALLSYMS_PARAMINFO_BTF is enabled and a function parameter
is a pointer to a kernel struct, use BTF type information to safely
dereference the pointer and display struct member values in oops/WARN
dumps.

The rendering uses btf_type_snprintf_show() which internally uses
copy_from_kernel_nofault() for safe memory access, making it safe to
call in oops/panic context.  Struct members are printed in a compact
two-column layout for readability.

Example output:

  Function parameters (paraminfo_demo_crash):
    file     (struct file *)         = 0xffff8bc043cb36c0
      .f_mode = (fmode_t)67993630               .f_flags = (unsigned int)32769
      .f_mapping = (struct address_space *)0x..  .f_inode = (struct inode *)0x..
      .f_cred = (struct cred *)0x...             .prev_pos = (loff_t)-1

Gated behind CONFIG_KALLSYMS_PARAMINFO_BTF which depends on both
CONFIG_KALLSYMS_PARAMINFO and CONFIG_DEBUG_INFO_BTF.  No additional
kernel image size beyond BTF itself.

Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
 init/Kconfig                    |  19 +++
 kernel/Makefile                 |   1 +
 kernel/kallsyms.c               |  47 ++++--
 kernel/kallsyms_paraminfo_btf.c | 267 ++++++++++++++++++++++++++++++++
 lib/tests/paraminfo_kunit.c     |  13 +-
 scripts/gen_paraminfo.c         |  80 ++++++++--
 6 files changed, 384 insertions(+), 43 deletions(-)
 create mode 100644 kernel/kallsyms_paraminfo_btf.c

diff --git a/init/Kconfig b/init/Kconfig
index 76d0c2da7d612..602594c86bf7e 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2106,6 +2106,25 @@ config KALLSYMS_PARAMINFO
 
 	  If unsure, say N.
 
+config KALLSYMS_PARAMINFO_BTF
+	bool "Render struct contents for pointer parameters in oops dumps"
+	depends on KALLSYMS_PARAMINFO && DEBUG_INFO_BTF
+	help
+	  When a function parameter is a pointer to a kernel struct and BTF
+	  type information is available, dereference the pointer and display
+	  key struct members (1 level deep) in oops/WARN dumps.
+
+	  When enabled, oops dumps may include additional indented lines
+	  showing struct member values in a two-column layout:
+
+	    file   (struct file *)         = 0xffff888123456000
+	      .f_flags = (unsigned int)32769  .f_mode = (fmode_t)29
+
+	  Requires CONFIG_DEBUG_INFO_BTF.
+	  No additional kernel image size beyond BTF itself.
+
+	  If unsure, say N.
+
 # end of the "standard kernel features (expert users)" menu
 
 config ARCH_HAS_MEMBARRIER_CALLBACKS
diff --git a/kernel/Makefile b/kernel/Makefile
index 6785982013dce..e47d911340cad 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -75,6 +75,7 @@ obj-$(CONFIG_UID16) += uid16.o
 obj-$(CONFIG_MODULE_SIG_FORMAT) += module_signature.o
 obj-$(CONFIG_KALLSYMS) += kallsyms.o
 obj-$(CONFIG_KALLSYMS_SELFTEST) += kallsyms_selftest.o
+obj-$(CONFIG_KALLSYMS_PARAMINFO_BTF) += kallsyms_paraminfo_btf.o
 obj-$(CONFIG_BSD_PROCESS_ACCT) += acct.o
 obj-$(CONFIG_VMCORE_INFO) += vmcore_info.o elfcorehdr.o
 obj-$(CONFIG_CRASH_RESERVE) += crash_reserve.o
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index af8de3d8e3ba3..362cfa80b2c08 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -510,16 +510,19 @@ bool kallsyms_lookup_lineinfo(unsigned long addr,
 
 #define MAX_PARAMINFO_PARAMS 6
 
+#ifdef CONFIG_KALLSYMS_PARAMINFO_BTF
+void paraminfo_btf_show_ptr(unsigned long ptr_val, const char *type_str);
+#else
+static inline void paraminfo_btf_show_ptr(unsigned long ptr_val,
+					   const char *type_str) {}
+#endif
+
 /*
  * x86-64 calling convention: arguments are passed in registers
  * RDI, RSI, RDX, RCX, R8, R9 (in that order).
  */
 #ifdef CONFIG_X86_64
 
-static const char * const paraminfo_reg_names[] = {
-	"RDI", "RSI", "RDX", "RCX", "R8", "R9"
-};
-
 static unsigned long paraminfo_get_reg(const struct pt_regs *regs,
 				       unsigned int idx)
 {
@@ -534,9 +537,6 @@ static unsigned long paraminfo_get_reg(const struct pt_regs *regs,
 	}
 }
 #else
-/* Stub for non-x86-64 architectures */
-static const char * const paraminfo_reg_names[] = {};
-
 static unsigned long paraminfo_get_reg(const struct pt_regs *regs,
 				       unsigned int idx)
 {
@@ -586,13 +586,13 @@ void kallsyms_show_paraminfo(struct pt_regs *regs)
 	const u8 *data;
 	unsigned int num_params, i;
 	unsigned long ip, fault_addr;
-	char sym_name[KSYM_NAME_LEN];
+	char sym_name[128];
 	unsigned long sym_size, sym_offset;
 
 	if (!regs || !paraminfo_num_funcs)
 		return;
 
-	ip = regs->ip;
+	ip = instruction_pointer(regs);
 
 	/* Only handle kernel-mode faults */
 	if (user_mode(regs))
@@ -611,14 +611,23 @@ void kallsyms_show_paraminfo(struct pt_regs *regs)
 		return;
 
 	/*
-	 * Verify the IP is within a reasonable range of the function
-	 * start.  paraminfo_func_addrs[] contains function start offsets;
-	 * check that we're not too far past the start.  Use kallsyms to
-	 * verify we're in the right function.
+	 * Verify the paraminfo entry actually matches the function
+	 * containing the IP.  Without this, if the faulting function
+	 * has no paraminfo, the binary search silently returns the
+	 * preceding function's entry — showing wrong parameter info.
 	 */
 	if (!kallsyms_lookup_size_offset(ip, &sym_size, &sym_offset))
 		return;
 
+	{
+		unsigned int func_start_offset;
+
+		func_start_offset = (unsigned int)(ip - sym_offset -
+						   (unsigned long)_text);
+		if (paraminfo_func_addrs[func_idx] != func_start_offset)
+			return;
+	}
+
 	/* Decode the function's parameter data */
 	data = paraminfo_func_data + paraminfo_func_offsets[func_idx];
 	num_params = *data++;
@@ -631,9 +640,9 @@ void kallsyms_show_paraminfo(struct pt_regs *regs)
 		return;
 
 	/*
-	 * Read the fault address for highlighting.  On x86, CR2 holds
-	 * the page fault linear address.  On other architectures this
-	 * would need a different mechanism.
+	 * Read CR2 for fault address highlighting.  CR2 is only meaningful
+	 * for page faults; for GPF, BUG, WARN, etc. it may hold a stale
+	 * value.  This is best-effort — a false match is harmless.
 	 */
 #ifdef CONFIG_X86
 	fault_addr = read_cr2();
@@ -660,9 +669,13 @@ void kallsyms_show_paraminfo(struct pt_regs *regs)
 
 		is_fault_addr = fault_addr && (val == fault_addr);
 
-		printk(KERN_DEFAULT "  %-8s (%-20s) = 0x%016lx%s\n",
+		printk(KERN_DEFAULT " %-8s (%-20s) = 0x%016lx%s\n",
 		       pname, ptype, val,
 		       is_fault_addr ? "  <-- fault address" : "");
+
+		/* If this is a pointer to a struct, try BTF deep rendering */
+		if (val && strstr(ptype, "*"))
+			paraminfo_btf_show_ptr(val, ptype);
 	}
 }
 EXPORT_SYMBOL_GPL(kallsyms_show_paraminfo);
diff --git a/kernel/kallsyms_paraminfo_btf.c b/kernel/kallsyms_paraminfo_btf.c
new file mode 100644
index 0000000000000..28ce1dd45f7a8
--- /dev/null
+++ b/kernel/kallsyms_paraminfo_btf.c
@@ -0,0 +1,267 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * kallsyms_paraminfo_btf.c - BTF-based deep rendering for paraminfo
+ *
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * When CONFIG_KALLSYMS_PARAMINFO_BTF is enabled and a function parameter
+ * is a pointer to a kernel struct, this module uses BTF type information
+ * to safely dereference the pointer and display key struct members in
+ * oops/WARN dumps.
+ */
+
+#include <linux/btf.h>
+#include <linux/bpf.h>
+#include <linux/kallsyms.h>
+#include <linux/kernel.h>
+#include <linux/printk.h>
+#include <linux/string.h>
+#include <linux/uaccess.h>
+
+/* Declared in kernel/kallsyms.c under CONFIG_KALLSYMS_PARAMINFO_BTF */
+void paraminfo_btf_show_ptr(unsigned long ptr_val, const char *type_str);
+
+extern struct btf *btf_vmlinux;
+
+/*
+ * Maximum output buffer for BTF rendering.  Large structs (e.g.,
+ * struct dentry) need substantial space.  This is declared static
+ * rather than on the stack because 4096 bytes would exceed the
+ * frame size limit.  Oops context is effectively single-threaded
+ * (other CPUs are stopped or spinning), so a static buffer is safe.
+ */
+#define BTF_SHOW_BUF_LEN	4096
+
+/*
+ * Parse a type string like "struct file *" to extract the struct name.
+ * Writes into caller-provided @name_buf of size @bufsz.
+ * Returns @name_buf on success, or NULL if not a struct/union pointer type.
+ */
+static const char *extract_struct_name(const char *type_str, bool *is_union,
+				       char *name_buf, size_t bufsz)
+{
+	const char *p, *end;
+
+	*is_union = false;
+
+	/* Must end with " *" to be a pointer */
+	end = type_str + strlen(type_str);
+	if (end - type_str < 3 || end[-1] != '*' || end[-2] != ' ')
+		return NULL;
+
+	if (!strncmp(type_str, "struct ", 7)) {
+		p = type_str + 7;
+	} else if (!strncmp(type_str, "union ", 6)) {
+		p = type_str + 6;
+		*is_union = true;
+	} else {
+		return NULL;
+	}
+
+	/* Copy name up to the " *" */
+	{
+		size_t len = (end - 2) - p;
+
+		if (len == 0 || len >= bufsz)
+			return NULL;
+		memcpy(name_buf, p, len);
+		name_buf[len] = '\0';
+	}
+
+	return name_buf;
+}
+
+/*
+ * Show BTF-rendered struct contents for a pointer parameter.
+ * Called from kallsyms_show_paraminfo() when the parameter is a
+ * struct/union pointer.
+ *
+ * Uses btf_type_snprintf_show() which internally uses
+ * copy_from_kernel_nofault() for safe memory access, making it
+ * safe to call in oops/panic context.
+ */
+void paraminfo_btf_show_ptr(unsigned long ptr_val, const char *type_str)
+{
+	static char buf[BTF_SHOW_BUF_LEN];
+	char sname[64];
+	const char *name;
+	bool is_union;
+	s32 type_id;
+	int ret;
+
+	if (!btf_vmlinux || !ptr_val)
+		return;
+
+	/* Only handle kernel pointers */
+	if (ptr_val < PAGE_OFFSET)
+		return;
+
+	name = extract_struct_name(type_str, &is_union, sname, sizeof(sname));
+	if (!name)
+		return;
+
+	type_id = btf_find_by_name_kind(btf_vmlinux, name,
+					is_union ? BTF_KIND_UNION
+						 : BTF_KIND_STRUCT);
+	if (type_id < 0)
+		return;
+
+	/*
+	 * Render without BTF_SHOW_COMPACT so each member gets its own
+	 * line with proper indentation from BTF.  Use BTF_SHOW_PTR_RAW
+	 * to print real kernel addresses instead of hashed pointers —
+	 * this is oops context where address visibility is critical.
+	 */
+	ret = btf_type_snprintf_show(btf_vmlinux, type_id, (void *)ptr_val,
+				     buf, sizeof(buf), BTF_SHOW_PTR_RAW);
+	if (ret < 0)
+		return;
+
+	buf[sizeof(buf) - 1] = '\0';
+
+	/*
+	 * Filter the multi-line BTF output: skip lines that contain
+	 * only braces/brackets/whitespace (structural noise), collect
+	 * meaningful member lines, and print them two per row.
+	 */
+	{
+		/*
+		 * Collect filtered lines as pointers into buf[] (which
+		 * we NUL-terminate in place).  Stack budget: ~64 pointers.
+		 */
+#define MAX_BTF_LINES 64
+#define BTF_COL_WIDTH 40
+		char *lines[MAX_BTF_LINES];
+		int nlines = 0;
+		char *line, *next;
+
+		for (line = buf; line && *line; line = next) {
+			char *s, *c;
+			bool has_content = false;
+
+			next = strchr(line, '\n');
+			if (next)
+				*next++ = '\0';
+
+			s = line;
+			while (*s == ' ' || *s == '\t')
+				s++;
+
+			/* Skip structural-only lines: {}[](), */
+			for (c = s; *c; c++) {
+				if (*c != '{' && *c != '}' &&
+				    *c != '(' && *c != ')' &&
+				    *c != '[' && *c != ']' &&
+				    *c != ',' && *c != ' ' &&
+				    *c != '\t') {
+					has_content = true;
+					break;
+				}
+			}
+			if (!has_content)
+				continue;
+
+			/* Skip type-only prefix lines */
+			if (*s == '(' && !strchr(s, '=') && !strchr(s, '['))
+				continue;
+
+			/* Trim trailing commas/spaces */
+			{
+				size_t len = strlen(s);
+
+				while (len > 0 && (s[len - 1] == ','
+						|| s[len - 1] == ' '))
+					s[--len] = '\0';
+			}
+
+			if (*s && nlines < MAX_BTF_LINES)
+				lines[nlines++] = s;
+		}
+
+		/*
+		 * Coalesce char array elements into strings.
+		 *
+		 * BTF renders char[] as individual elements:
+		 *   .sysname = (char[])[
+		 *   'L'
+		 *   'i'
+		 *   'n'  ...
+		 *
+		 * Detect lines containing "= (char[])[" or
+		 * "= (unsigned char[])[" and collect following
+		 * single-quoted-char lines into a readable string
+		 * like: .sysname = "Linux"
+		 */
+#define MAX_COALESCED 8
+		{
+			static char coalesced[MAX_COALESCED][128];
+			int ci = 0, i;
+
+			for (i = 0; i < nlines && ci < MAX_COALESCED; i++) {
+				char *eq;
+				int spos, j, pfxlen;
+
+				eq = strstr(lines[i], "(char[])[");
+				if (!eq)
+					eq = strstr(lines[i], "(unsigned char[])[");
+				if (!eq)
+					continue;
+
+				/* Extract prefix up to and including '=' */
+				eq = strstr(lines[i], "= ");
+				if (!eq)
+					continue;
+				pfxlen = eq - lines[i] + 2;
+				if (pfxlen > 60)
+					pfxlen = 60;
+
+				memcpy(coalesced[ci], lines[i], pfxlen);
+				coalesced[ci][pfxlen] = '"';
+				spos = pfxlen + 1;
+
+				/* Gather chars from subsequent lines */
+				for (j = i + 1; j < nlines &&
+				     spos < (int)sizeof(coalesced[0]) - 2; j++) {
+					char *s = lines[j];
+
+					if (s[0] == '\'' && s[2] == '\'' &&
+					    (s[3] == '\0' || s[3] == ',')) {
+						coalesced[ci][spos++] = s[1];
+						lines[j] = "";
+					} else {
+						break;
+					}
+				}
+				coalesced[ci][spos++] = '"';
+				coalesced[ci][spos] = '\0';
+				lines[i] = coalesced[ci];
+				ci++;
+			}
+		}
+#undef MAX_COALESCED
+
+		/* Print in two columns, skipping empty (consumed) lines */
+		{
+			int i, col = 0;
+			char *pending = NULL;
+
+			for (i = 0; i < nlines; i++) {
+				if (!lines[i][0])
+					continue;
+				if (col == 0) {
+					pending = lines[i];
+					col = 1;
+				} else {
+					printk(KERN_DEFAULT "  %-*s  %s\n",
+					       BTF_COL_WIDTH, pending,
+					       lines[i]);
+					col = 0;
+				}
+			}
+			if (col == 1)
+				printk(KERN_DEFAULT "  %s\n", pending);
+		}
+#undef MAX_BTF_LINES
+#undef BTF_COL_WIDTH
+	}
+}
diff --git a/lib/tests/paraminfo_kunit.c b/lib/tests/paraminfo_kunit.c
index e09efc4ddeb0e..74a4436163a98 100644
--- a/lib/tests/paraminfo_kunit.c
+++ b/lib/tests/paraminfo_kunit.c
@@ -7,7 +7,8 @@
  * Verifies that the paraminfo tables correctly map function addresses
  * to their parameter names and types.
  *
- * Build with: CONFIG_PARAMINFO_KUNIT_TEST=m (or =y)
+ * Build with: CONFIG_PARAMINFO_KUNIT_TEST=y (must be built-in; paraminfo
+ * tables are vmlinux-only, so module test functions won't be found)
  */
 
 #include <kunit/test.h>
@@ -45,15 +46,7 @@ static noinline void paraminfo_test_no_args(void)
 
 /* ---- Helpers to query paraminfo tables directly ---- */
 
-/*
- * These access the raw paraminfo tables to verify correctness.
- * The tables are defined in kernel/kallsyms_internal.h.
- */
-extern const u32 paraminfo_num_funcs;
-extern const u32 paraminfo_func_addrs[];
-extern const u32 paraminfo_func_offsets[];
-extern const u8  paraminfo_func_data[];
-extern const char paraminfo_strings[];
+#include "../../kernel/kallsyms_internal.h"
 
 struct param_result {
 	unsigned int num_params;
diff --git a/scripts/gen_paraminfo.c b/scripts/gen_paraminfo.c
index ea1d23f3ddd9a..b64dd1232c77c 100644
--- a/scripts/gen_paraminfo.c
+++ b/scripts/gen_paraminfo.c
@@ -58,7 +58,7 @@ static unsigned int num_strings;
 static unsigned int strtab_capacity;
 static unsigned int strtab_total_size;
 
-#define STR_HASH_BITS 14
+#define STR_HASH_BITS 18
 #define STR_HASH_SIZE (1 << STR_HASH_BITS)
 
 struct str_hash_entry {
@@ -87,6 +87,13 @@ static unsigned int find_or_add_string(const char *s)
 		h = (h + 1) & (STR_HASH_SIZE - 1);
 	}
 
+	if (num_strings >= STR_HASH_SIZE * 3 / 4) {
+		fprintf(stderr,
+			"gen_paraminfo: string hash table overflow (%u entries)\n",
+			num_strings);
+		exit(1);
+	}
+
 	if (num_strings >= strtab_capacity) {
 		strtab_capacity = strtab_capacity ? strtab_capacity * 2 : 8192;
 		strtab = realloc(strtab, strtab_capacity * sizeof(*strtab));
@@ -97,6 +104,10 @@ static unsigned int find_or_add_string(const char *s)
 	}
 
 	strtab[num_strings].str = strdup(s);
+	if (!strtab[num_strings].str) {
+		fprintf(stderr, "out of memory\n");
+		exit(1);
+	}
 	strtab[num_strings].offset = strtab_total_size;
 	strtab_total_size += strlen(s) + 1;
 
@@ -120,20 +131,32 @@ static void add_func(struct func_entry *f)
 	funcs[num_funcs++] = *f;
 }
 
+/* Max recursion depth to prevent stack overflow on pathological DWARF */
+#define MAX_TYPE_DEPTH 16
+
+static void __build_type_name(Dwarf_Die *type_die, char *buf, size_t bufsz,
+			      int depth);
+
 /*
  * Build a human-readable type name string from a DWARF type DIE.
  * Follows the type chain (pointers, const, etc.) to produce strings like:
  *   "struct file *", "const char *", "unsigned long", "void *"
  */
 static void build_type_name(Dwarf_Die *type_die, char *buf, size_t bufsz)
+{
+	__build_type_name(type_die, buf, bufsz, 0);
+}
+
+static void __build_type_name(Dwarf_Die *type_die, char *buf, size_t bufsz,
+			      int depth)
 {
 	Dwarf_Die child;
 	Dwarf_Attribute attr;
 	const char *name;
 	int tag;
 
-	if (!type_die) {
-		snprintf(buf, bufsz, "void");
+	if (!type_die || depth > MAX_TYPE_DEPTH) {
+		snprintf(buf, bufsz, depth > MAX_TYPE_DEPTH ? "..." : "void");
 		return;
 	}
 
@@ -148,7 +171,7 @@ static void build_type_name(Dwarf_Die *type_die, char *buf, size_t bufsz)
 	case DW_TAG_pointer_type:
 		if (dwarf_attr(type_die, DW_AT_type, &attr) &&
 		    dwarf_formref_die(&attr, &child)) {
-			build_type_name(&child, buf, bufsz);
+			__build_type_name(&child, buf, bufsz, depth + 1);
 			if (strlen(buf) + 3 < bufsz)
 				strcat(buf, " *");
 		} else {
@@ -161,7 +184,7 @@ static void build_type_name(Dwarf_Die *type_die, char *buf, size_t bufsz)
 		    dwarf_formref_die(&attr, &child)) {
 			char tmp[MAX_TYPE_LEN - 10];
 
-			build_type_name(&child, tmp, sizeof(tmp));
+			__build_type_name(&child, tmp, sizeof(tmp), depth + 1);
 			snprintf(buf, bufsz, "const %s", tmp);
 		} else {
 			snprintf(buf, bufsz, "const void");
@@ -173,7 +196,7 @@ static void build_type_name(Dwarf_Die *type_die, char *buf, size_t bufsz)
 		    dwarf_formref_die(&attr, &child)) {
 			char tmp[MAX_TYPE_LEN - 10];
 
-			build_type_name(&child, tmp, sizeof(tmp));
+			__build_type_name(&child, tmp, sizeof(tmp), depth + 1);
 			snprintf(buf, bufsz, "volatile %s", tmp);
 		} else {
 			snprintf(buf, bufsz, "volatile void");
@@ -183,7 +206,7 @@ static void build_type_name(Dwarf_Die *type_die, char *buf, size_t bufsz)
 	case DW_TAG_restrict_type:
 		if (dwarf_attr(type_die, DW_AT_type, &attr) &&
 		    dwarf_formref_die(&attr, &child)) {
-			build_type_name(&child, buf, bufsz);
+			__build_type_name(&child, buf, bufsz, depth + 1);
 		} else {
 			snprintf(buf, bufsz, "void");
 		}
@@ -195,7 +218,7 @@ static void build_type_name(Dwarf_Die *type_die, char *buf, size_t bufsz)
 			snprintf(buf, bufsz, "%s", name);
 		} else if (dwarf_attr(type_die, DW_AT_type, &attr) &&
 			   dwarf_formref_die(&attr, &child)) {
-			build_type_name(&child, buf, bufsz);
+			__build_type_name(&child, buf, bufsz, depth + 1);
 		} else {
 			snprintf(buf, bufsz, "?");
 		}
@@ -219,7 +242,7 @@ static void build_type_name(Dwarf_Die *type_die, char *buf, size_t bufsz)
 	case DW_TAG_array_type:
 		if (dwarf_attr(type_die, DW_AT_type, &attr) &&
 		    dwarf_formref_die(&attr, &child)) {
-			build_type_name(&child, buf, bufsz);
+			__build_type_name(&child, buf, bufsz, depth + 1);
 			if (strlen(buf) + 3 < bufsz)
 				strcat(buf, "[]");
 		} else {
@@ -231,8 +254,23 @@ static void build_type_name(Dwarf_Die *type_die, char *buf, size_t bufsz)
 		snprintf(buf, bufsz, "func_ptr");
 		break;
 
+	case DW_TAG_unspecified_type:
+		name = dwarf_diename(type_die);
+		snprintf(buf, bufsz, "%s", name ? name : "void");
+		break;
+
 	default:
-		snprintf(buf, bufsz, "?");
+		/*
+		 * Unknown tag — try to follow DW_AT_type if present
+		 * (handles DW_TAG_atomic_type and others).
+		 */
+		if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+		    dwarf_formref_die(&attr, &child)) {
+			__build_type_name(&child, buf, bufsz, depth + 1);
+		} else {
+			name = dwarf_diename(type_die);
+			snprintf(buf, bufsz, "%s", name ? name : "?");
+		}
 		break;
 	}
 }
@@ -311,10 +349,15 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
 				continue;
 
 			/* Skip declarations (no body) */
-			if (dwarf_attr(&child, DW_AT_declaration, &attr))
+			if (dwarf_attr_integrate(&child, DW_AT_declaration, &attr))
 				continue;
 
-			/* Get function start address */
+			/*
+			 * Get function start address.
+			 * dwarf_lowpc handles DW_AT_low_pc directly, but
+			 * for concrete inlined instances the address may be
+			 * in a ranges table.
+			 */
 			if (dwarf_lowpc(&child, &low_pc) != 0)
 				continue;
 
@@ -341,6 +384,12 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
 					if (func.num_params >= MAX_PARAMS)
 						break;
 
+					/*
+					 * Use dwarf_attr_integrate to follow
+					 * DW_AT_abstract_origin chains — inlined
+					 * or outlined functions may store param
+					 * names/types in an abstract instance.
+					 */
 					pname = dwarf_diename(&param);
 					if (!pname)
 						pname = "?";
@@ -349,8 +398,8 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
 						 sizeof(func.params[0].name),
 						 "%s", pname);
 
-					/* Resolve type */
-					if (dwarf_attr(&param, DW_AT_type, &attr) &&
+					/* Resolve type (follow abstract origin) */
+					if (dwarf_attr_integrate(&param, DW_AT_type, &attr) &&
 					    dwarf_formref_die(&attr, &type_die)) {
 						build_type_name(&type_die,
 								func.params[func.num_params].type,
@@ -530,8 +579,7 @@ int main(int argc, char *argv[])
 	process_dwarf(dwarf, text_addr);
 	deduplicate();
 
-	fprintf(stderr, "paraminfo: %u functions, %u strings\n",
-		num_funcs, num_strings);
+	fprintf(stderr, "paraminfo: %u functions\n", num_funcs);
 
 	output_assembly();
 
-- 
2.51.0


^ permalink raw reply related

* [PATCH 1/2] kallsyms: show function parameter info in oops/WARN dumps
From: Sasha Levin @ 2026-03-23 16:48 UTC (permalink / raw)
  To: Andrew Morton, Masahiro Yamada, Nathan Chancellor, Nicolas Schier
  Cc: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen,
	H. Peter Anvin, Peter Zijlstra, Josh Poimboeuf, Petr Mladek,
	Alexei Starovoitov, Jonathan Corbet, David Gow, Kees Cook,
	Greg KH, Luis Chamberlain, Steven Rostedt, Helge Deller,
	Randy Dunlap, Geert Uytterhoeven, Juergen Gross, James Bottomley,
	Alexey Dobriyan, Vlastimil Babka, Laurent Pinchart, Petr Pavlu,
	x86, linux-kernel, linux-kbuild, linux-doc, linux-modules, bpf,
	Sasha Levin
In-Reply-To: <20260323164858.1939248-1-sashal@kernel.org>

Embed DWARF-derived function parameter name and type information in the
kernel image so that oops and WARN dumps display the crashing function's
register-passed arguments with their names, types, and values.

A new build-time tool (scripts/gen_paraminfo.c) parses DW_TAG_subprogram
and DW_TAG_formal_parameter entries from DWARF .debug_info, extracting
parameter names and human-readable type strings. The resulting tables are
stored in .rodata using the same two-phase link approach as lineinfo.

At runtime, kallsyms_show_paraminfo() performs a binary search on the
paraminfo tables, maps parameters to x86-64 calling convention registers
(RDI, RSI, RDX, RCX, R8, R9), and prints each parameter's name, type,
and value from pt_regs. If a parameter value matches the page fault
address (CR2), it is highlighted with "<-- fault address".

Integration at show_regs() means this works for both oops and WARN()
automatically, since both paths provide full pt_regs at the exception
point.

Example output:

  Function parameters (ext4_readdir):
    file     (struct file *)         = 0xffff888123456000
    ctx      (struct dir_context *)  = 0x0000000000001234  <-- fault address

Gated behind CONFIG_KALLSYMS_PARAMINFO (depends on CONFIG_KALLSYMS_LINEINFO).
Adds approximately 1-2 MB to the kernel image for ~58K functions.

Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
 .../admin-guide/kallsyms-lineinfo.rst         |  31 +
 arch/x86/kernel/dumpstack.c                   |   6 +-
 include/linux/kallsyms.h                      |   9 +
 init/Kconfig                                  |  21 +
 kernel/kallsyms.c                             | 168 ++++++
 kernel/kallsyms_internal.h                    |   6 +
 lib/Kconfig.debug                             |  11 +
 lib/tests/Makefile                            |   3 +
 lib/tests/paraminfo_kunit.c                   | 256 ++++++++
 scripts/Makefile                              |   3 +
 scripts/empty_paraminfo.S                     |  18 +
 scripts/gen_paraminfo.c                       | 549 ++++++++++++++++++
 scripts/link-vmlinux.sh                       |  44 +-
 13 files changed, 1119 insertions(+), 6 deletions(-)
 create mode 100644 lib/tests/paraminfo_kunit.c
 create mode 100644 scripts/empty_paraminfo.S
 create mode 100644 scripts/gen_paraminfo.c

diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
index dd264830c8d5b..26921bb3f7f81 100644
--- a/Documentation/admin-guide/kallsyms-lineinfo.rst
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -83,6 +83,37 @@ compression).
 Per-module lineinfo adds approximately 2-3 bytes per DWARF line entry to each
 ``.ko`` file.
 
+Function Parameter Info
+======================
+
+``CONFIG_KALLSYMS_PARAMINFO`` extends the debugging information by embedding
+function parameter names and types in the kernel image.  When an oops or WARN
+occurs, the faulting function's register-passed arguments are displayed with
+their names, types, and values from the saved registers.
+
+Enable in addition to the lineinfo options::
+
+    CONFIG_KALLSYMS_PARAMINFO=y
+
+Example oops output with paraminfo::
+
+    RIP: 0010:ext4_readdir+0x1a3/0x5b0 (fs/ext4/dir.c:421)
+    ...
+    Function parameters (ext4_readdir):
+      file     (struct file *)         = 0xffff888123456000
+      ctx      (struct dir_context *)  = 0x0000000000001234  <-- fault address
+
+The ``<-- fault address`` annotation appears when a parameter value matches
+the page fault address (CR2 on x86), helping quickly identify which argument
+caused the crash.
+
+This feature works for both oops and WARN() on x86-64.  Only register-passed
+parameters (up to 6 on x86-64: RDI, RSI, RDX, RCX, R8, R9) are displayed.
+The parameter info is only shown for the faulting/warning frame where full
+register state is available.
+
+The paraminfo tables add approximately 1-2 MiB to the kernel image.
+
 Known Limitations
 =================
 
diff --git a/arch/x86/kernel/dumpstack.c b/arch/x86/kernel/dumpstack.c
index b10684dedc589..4e9b5fd58fd1b 100644
--- a/arch/x86/kernel/dumpstack.c
+++ b/arch/x86/kernel/dumpstack.c
@@ -483,8 +483,10 @@ void show_regs(struct pt_regs *regs)
 	__show_regs(regs, print_kernel_regs, KERN_DEFAULT);
 
 	/*
-	 * When in-kernel, we also print out the stack at the time of the fault..
+	 * When in-kernel, show function parameter info and stack trace.
 	 */
-	if (!user_mode(regs))
+	if (!user_mode(regs)) {
+		kallsyms_show_paraminfo(regs);
 		show_trace_log_lvl(current, regs, NULL, KERN_DEFAULT);
+	}
 }
diff --git a/include/linux/kallsyms.h b/include/linux/kallsyms.h
index 7d4c9dca06c87..17c9df520b2b0 100644
--- a/include/linux/kallsyms.h
+++ b/include/linux/kallsyms.h
@@ -104,6 +104,13 @@ int lookup_symbol_name(unsigned long addr, char *symname);
 bool kallsyms_lookup_lineinfo(unsigned long addr,
 			      const char **file, unsigned int *line);
 
+#ifdef CONFIG_KALLSYMS_PARAMINFO
+struct pt_regs;
+void kallsyms_show_paraminfo(struct pt_regs *regs);
+#else
+static inline void kallsyms_show_paraminfo(struct pt_regs *regs) {}
+#endif
+
 #else /* !CONFIG_KALLSYMS */
 
 static inline unsigned long kallsyms_lookup_name(const char *name)
@@ -179,6 +186,8 @@ static inline bool kallsyms_lookup_lineinfo(unsigned long addr,
 {
 	return false;
 }
+
+static inline void kallsyms_show_paraminfo(struct pt_regs *regs) {}
 #endif /*CONFIG_KALLSYMS*/
 
 static inline void print_ip_sym(const char *loglvl, unsigned long ip)
diff --git a/init/Kconfig b/init/Kconfig
index 6e3795b3dbd62..76d0c2da7d612 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2085,6 +2085,27 @@ config KALLSYMS_LINEINFO_MODULES
 
 	  If unsure, say N.
 
+config KALLSYMS_PARAMINFO
+	bool "Show function parameter info in oops/WARN dumps"
+	depends on KALLSYMS_LINEINFO
+	help
+	  Embeds function parameter name and type information in the kernel
+	  image, extracted from DWARF debug info at build time.  When an
+	  oops or WARN occurs, the crashing/warning function's register-
+	  passed arguments are displayed with their names, types, and
+	  values from pt_regs.
+
+	  When enabled, oops/WARN dumps include lines like:
+
+	    Function parameters (ext4_readdir):
+	      file   (struct file *)         = 0xffff888123456000
+	      ctx    (struct dir_context *)  = 0x0000000000001234
+
+	  Requires elfutils (libdw-dev/elfutils-devel) on the build host.
+	  Adds approximately 1-2 MB to the kernel image.
+
+	  If unsure, say N.
+
 # end of the "standard kernel features (expert users)" menu
 
 config ARCH_HAS_MEMBARRIER_CALLBACKS
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index e6f796d43dd70..af8de3d8e3ba3 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -501,6 +501,174 @@ bool kallsyms_lookup_lineinfo(unsigned long addr,
 	return lineinfo_search(&tbl, (unsigned int)raw_offset, file, line);
 }
 
+#ifdef CONFIG_KALLSYMS_PARAMINFO
+
+#include <linux/ptrace.h>
+#ifdef CONFIG_X86
+#include <asm/special_insns.h>
+#endif
+
+#define MAX_PARAMINFO_PARAMS 6
+
+/*
+ * x86-64 calling convention: arguments are passed in registers
+ * RDI, RSI, RDX, RCX, R8, R9 (in that order).
+ */
+#ifdef CONFIG_X86_64
+
+static const char * const paraminfo_reg_names[] = {
+	"RDI", "RSI", "RDX", "RCX", "R8", "R9"
+};
+
+static unsigned long paraminfo_get_reg(const struct pt_regs *regs,
+				       unsigned int idx)
+{
+	switch (idx) {
+	case 0: return regs->di;
+	case 1: return regs->si;
+	case 2: return regs->dx;
+	case 3: return regs->cx;
+	case 4: return regs->r8;
+	case 5: return regs->r9;
+	default: return 0;
+	}
+}
+#else
+/* Stub for non-x86-64 architectures */
+static const char * const paraminfo_reg_names[] = {};
+
+static unsigned long paraminfo_get_reg(const struct pt_regs *regs,
+				       unsigned int idx)
+{
+	return 0;
+}
+#endif /* CONFIG_X86_64 */
+
+/*
+ * Binary search for the function containing the given offset in
+ * paraminfo_func_addrs[].  Returns the index of the function whose
+ * start address is <= offset, or -1 if not found.
+ */
+static int paraminfo_find_func(unsigned int offset)
+{
+	int lo = 0, hi = paraminfo_num_funcs - 1;
+	int result = -1;
+
+	if (!paraminfo_num_funcs)
+		return -1;
+
+	while (lo <= hi) {
+		int mid = lo + (hi - lo) / 2;
+
+		if (paraminfo_func_addrs[mid] <= offset) {
+			result = mid;
+			lo = mid + 1;
+		} else {
+			hi = mid - 1;
+		}
+	}
+
+	return result;
+}
+
+/*
+ * Show function parameter info for the faulting/warning instruction.
+ *
+ * Called from show_regs() on x86 when CONFIG_KALLSYMS_PARAMINFO is
+ * enabled.  Works for both oops (page fault, GPF, etc.) and WARN(),
+ * since both paths provide full pt_regs at the exception point.
+ */
+void kallsyms_show_paraminfo(struct pt_regs *regs)
+{
+	unsigned long long raw_offset;
+	unsigned int offset;
+	int func_idx;
+	const u8 *data;
+	unsigned int num_params, i;
+	unsigned long ip, fault_addr;
+	char sym_name[KSYM_NAME_LEN];
+	unsigned long sym_size, sym_offset;
+
+	if (!regs || !paraminfo_num_funcs)
+		return;
+
+	ip = regs->ip;
+
+	/* Only handle kernel-mode faults */
+	if (user_mode(regs))
+		return;
+
+	if (ip < (unsigned long)_text)
+		return;
+
+	raw_offset = ip - (unsigned long)_text;
+	if (raw_offset > UINT_MAX)
+		return;
+	offset = (unsigned int)raw_offset;
+
+	func_idx = paraminfo_find_func(offset);
+	if (func_idx < 0)
+		return;
+
+	/*
+	 * Verify the IP is within a reasonable range of the function
+	 * start.  paraminfo_func_addrs[] contains function start offsets;
+	 * check that we're not too far past the start.  Use kallsyms to
+	 * verify we're in the right function.
+	 */
+	if (!kallsyms_lookup_size_offset(ip, &sym_size, &sym_offset))
+		return;
+
+	/* Decode the function's parameter data */
+	data = paraminfo_func_data + paraminfo_func_offsets[func_idx];
+	num_params = *data++;
+
+	if (num_params == 0 || num_params > MAX_PARAMINFO_PARAMS)
+		return;
+
+	/* Look up function name for the header */
+	if (lookup_symbol_name(ip - sym_offset, sym_name))
+		return;
+
+	/*
+	 * Read the fault address for highlighting.  On x86, CR2 holds
+	 * the page fault linear address.  On other architectures this
+	 * would need a different mechanism.
+	 */
+#ifdef CONFIG_X86
+	fault_addr = read_cr2();
+#else
+	fault_addr = 0;
+#endif
+
+	printk(KERN_DEFAULT "Function parameters (%s):\n", sym_name);
+
+	for (i = 0; i < num_params; i++) {
+		u32 name_off, type_off;
+		const char *pname, *ptype;
+		unsigned long val;
+		bool is_fault_addr;
+
+		memcpy(&name_off, data, sizeof(u32));
+		data += sizeof(u32);
+		memcpy(&type_off, data, sizeof(u32));
+		data += sizeof(u32);
+
+		pname = paraminfo_strings + name_off;
+		ptype = paraminfo_strings + type_off;
+		val = paraminfo_get_reg(regs, i);
+
+		is_fault_addr = fault_addr && (val == fault_addr);
+
+		printk(KERN_DEFAULT "  %-8s (%-20s) = 0x%016lx%s\n",
+		       pname, ptype, val,
+		       is_fault_addr ? "  <-- fault address" : "");
+	}
+}
+EXPORT_SYMBOL_GPL(kallsyms_show_paraminfo);
+
+#endif /* CONFIG_KALLSYMS_PARAMINFO */
+
 /* Look up a kernel symbol and return it in a text buffer. */
 static int __sprint_symbol(char *buffer, unsigned long address,
 			   int symbol_offset, int add_offset, int add_buildid)
diff --git a/kernel/kallsyms_internal.h b/kernel/kallsyms_internal.h
index ffe4c658067ec..7287ee0859515 100644
--- a/kernel/kallsyms_internal.h
+++ b/kernel/kallsyms_internal.h
@@ -26,4 +26,10 @@ extern const u32 lineinfo_file_offsets[];
 extern const u32 lineinfo_filenames_size;
 extern const char lineinfo_filenames[];
 
+extern const u32 paraminfo_num_funcs;
+extern const u32 paraminfo_func_addrs[];
+extern const u32 paraminfo_func_offsets[];
+extern const u8  paraminfo_func_data[];
+extern const char paraminfo_strings[];
+
 #endif // LINUX_KALLSYMS_INTERNAL_H_
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 688bbcb3eaa62..be8cee0985fbd 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -3058,6 +3058,17 @@ config LINEINFO_KUNIT_TEST
 
 	  If unsure, say N.
 
+config PARAMINFO_KUNIT_TEST
+	tristate "KUnit tests for kallsyms paraminfo" if !KUNIT_ALL_TESTS
+	depends on KUNIT && KALLSYMS_PARAMINFO
+	default KUNIT_ALL_TESTS
+	help
+	  KUnit tests for the kallsyms function parameter info feature.
+	  Verifies that paraminfo tables correctly map functions to their
+	  parameter names and types.
+
+	  If unsure, say N.
+
 config HW_BREAKPOINT_KUNIT_TEST
 	bool "Test hw_breakpoint constraints accounting" if !KUNIT_ALL_TESTS
 	depends on HAVE_HW_BREAKPOINT
diff --git a/lib/tests/Makefile b/lib/tests/Makefile
index c6add3b04bbd5..70452942baf45 100644
--- a/lib/tests/Makefile
+++ b/lib/tests/Makefile
@@ -39,6 +39,9 @@ obj-$(CONFIG_LONGEST_SYM_KUNIT_TEST) += longest_symbol_kunit.o
 CFLAGS_lineinfo_kunit.o += $(call cc-option,-fno-inline-functions-called-once)
 obj-$(CONFIG_LINEINFO_KUNIT_TEST) += lineinfo_kunit.o
 
+CFLAGS_paraminfo_kunit.o += $(call cc-option,-fno-inline-functions-called-once)
+obj-$(CONFIG_PARAMINFO_KUNIT_TEST) += paraminfo_kunit.o
+
 obj-$(CONFIG_MEMCPY_KUNIT_TEST) += memcpy_kunit.o
 obj-$(CONFIG_MIN_HEAP_KUNIT_TEST) += min_heap_kunit.o
 CFLAGS_overflow_kunit.o = $(call cc-disable-warning, tautological-constant-out-of-range-compare)
diff --git a/lib/tests/paraminfo_kunit.c b/lib/tests/paraminfo_kunit.c
new file mode 100644
index 0000000000000..e09efc4ddeb0e
--- /dev/null
+++ b/lib/tests/paraminfo_kunit.c
@@ -0,0 +1,256 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for kallsyms paraminfo (CONFIG_KALLSYMS_PARAMINFO).
+ *
+ * Copyright (c) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Verifies that the paraminfo tables correctly map function addresses
+ * to their parameter names and types.
+ *
+ * Build with: CONFIG_PARAMINFO_KUNIT_TEST=m (or =y)
+ */
+
+#include <kunit/test.h>
+#include <linux/kallsyms.h>
+#include <linux/module.h>
+#include <linux/string.h>
+#include <linux/slab.h>
+
+/* ---- Test target functions with known signatures ---- */
+
+static noinline int paraminfo_test_two_args(struct kunit *test, int value)
+{
+	/* Prevent optimization */
+	return test ? value + 1 : 0;
+}
+
+static noinline void *paraminfo_test_ptr_arg(void *ptr, unsigned long size)
+{
+	if (ptr && size > 0)
+		return ptr;
+	return NULL;
+}
+
+static noinline long paraminfo_test_many_args(int a, int b, int c,
+					      int d, int e, int f)
+{
+	return (long)a + b + c + d + e + f;
+}
+
+static noinline void paraminfo_test_no_args(void)
+{
+	/* Function with no parameters */
+	barrier();
+}
+
+/* ---- Helpers to query paraminfo tables directly ---- */
+
+/*
+ * These access the raw paraminfo tables to verify correctness.
+ * The tables are defined in kernel/kallsyms_internal.h.
+ */
+extern const u32 paraminfo_num_funcs;
+extern const u32 paraminfo_func_addrs[];
+extern const u32 paraminfo_func_offsets[];
+extern const u8  paraminfo_func_data[];
+extern const char paraminfo_strings[];
+
+struct param_result {
+	unsigned int num_params;
+	const char *names[6];
+	const char *types[6];
+};
+
+/*
+ * Look up paraminfo for a given kernel address.
+ * Returns true if found, filling in @result.
+ */
+static bool lookup_paraminfo(unsigned long addr, struct param_result *result)
+{
+	unsigned long long raw_offset;
+	unsigned int offset;
+	int lo, hi, func_idx;
+	const u8 *data;
+	unsigned int i;
+
+	if (!paraminfo_num_funcs)
+		return false;
+
+	if (addr < (unsigned long)_text)
+		return false;
+
+	raw_offset = addr - (unsigned long)_text;
+	if (raw_offset > UINT_MAX)
+		return false;
+	offset = (unsigned int)raw_offset;
+
+	/* Binary search for the function */
+	lo = 0;
+	hi = paraminfo_num_funcs - 1;
+	func_idx = -1;
+	while (lo <= hi) {
+		int mid = lo + (hi - lo) / 2;
+
+		if (paraminfo_func_addrs[mid] <= offset) {
+			func_idx = mid;
+			lo = mid + 1;
+		} else {
+			hi = mid - 1;
+		}
+	}
+
+	if (func_idx < 0)
+		return false;
+
+	/* Verify we're not too far from the function start */
+	if (offset - paraminfo_func_addrs[func_idx] > 0x10000)
+		return false;
+
+	data = paraminfo_func_data + paraminfo_func_offsets[func_idx];
+	result->num_params = *data++;
+
+	if (result->num_params > 6)
+		return false;
+
+	for (i = 0; i < result->num_params; i++) {
+		u32 name_off, type_off;
+
+		memcpy(&name_off, data, sizeof(u32));
+		data += sizeof(u32);
+		memcpy(&type_off, data, sizeof(u32));
+		data += sizeof(u32);
+
+		result->names[i] = paraminfo_strings + name_off;
+		result->types[i] = paraminfo_strings + type_off;
+	}
+
+	return true;
+}
+
+/* ---- Test cases ---- */
+
+static void test_paraminfo_two_args(struct kunit *test)
+{
+	struct param_result res;
+	bool found;
+
+	found = lookup_paraminfo((unsigned long)paraminfo_test_two_args, &res);
+
+	if (!IS_ENABLED(CONFIG_KALLSYMS_PARAMINFO)) {
+		KUNIT_EXPECT_FALSE(test, found);
+		return;
+	}
+
+	KUNIT_ASSERT_TRUE(test, found);
+	KUNIT_EXPECT_EQ(test, res.num_params, 2U);
+	KUNIT_EXPECT_STREQ(test, res.names[0], "test");
+	KUNIT_EXPECT_STREQ(test, res.names[1], "value");
+	KUNIT_EXPECT_TRUE(test, strstr(res.types[1], "int") != NULL);
+}
+
+static void test_paraminfo_ptr_arg(struct kunit *test)
+{
+	struct param_result res;
+	bool found;
+
+	found = lookup_paraminfo((unsigned long)paraminfo_test_ptr_arg, &res);
+
+	if (!IS_ENABLED(CONFIG_KALLSYMS_PARAMINFO)) {
+		KUNIT_EXPECT_FALSE(test, found);
+		return;
+	}
+
+	KUNIT_ASSERT_TRUE(test, found);
+	KUNIT_EXPECT_EQ(test, res.num_params, 2U);
+	KUNIT_EXPECT_STREQ(test, res.names[0], "ptr");
+	KUNIT_EXPECT_STREQ(test, res.names[1], "size");
+	/* First param should be a pointer type */
+	KUNIT_EXPECT_TRUE(test, strstr(res.types[0], "*") != NULL);
+}
+
+static void test_paraminfo_many_args(struct kunit *test)
+{
+	struct param_result res;
+	bool found;
+
+	found = lookup_paraminfo((unsigned long)paraminfo_test_many_args, &res);
+
+	if (!IS_ENABLED(CONFIG_KALLSYMS_PARAMINFO)) {
+		KUNIT_EXPECT_FALSE(test, found);
+		return;
+	}
+
+	KUNIT_ASSERT_TRUE(test, found);
+	KUNIT_EXPECT_EQ(test, res.num_params, 6U);
+	KUNIT_EXPECT_STREQ(test, res.names[0], "a");
+	KUNIT_EXPECT_STREQ(test, res.names[1], "b");
+	KUNIT_EXPECT_STREQ(test, res.names[2], "c");
+	KUNIT_EXPECT_STREQ(test, res.names[3], "d");
+	KUNIT_EXPECT_STREQ(test, res.names[4], "e");
+	KUNIT_EXPECT_STREQ(test, res.names[5], "f");
+}
+
+static void test_paraminfo_no_args(struct kunit *test)
+{
+	struct param_result res;
+	bool found;
+
+	/*
+	 * Functions with no parameters should not have entries in the
+	 * paraminfo table (they are filtered out at build time).
+	 * The lookup may find a nearby function instead, but if it
+	 * does find our function exactly, num_params should be 0.
+	 */
+	found = lookup_paraminfo((unsigned long)paraminfo_test_no_args, &res);
+
+	if (found) {
+		/* If it matched our exact function, it should have 0 params.
+		 * But it may have matched a preceding function instead.
+		 */
+		kunit_info(test, "lookup found func with %u params\n",
+			   res.num_params);
+	}
+}
+
+static void test_paraminfo_bogus_addr(struct kunit *test)
+{
+	struct param_result res;
+
+	/* Address 0 should not match anything */
+	KUNIT_EXPECT_FALSE(test, lookup_paraminfo(0, &res));
+
+	/* Address in userspace should not match */
+	KUNIT_EXPECT_FALSE(test, lookup_paraminfo(0x1000, &res));
+}
+
+static void test_paraminfo_tables_present(struct kunit *test)
+{
+	if (!IS_ENABLED(CONFIG_KALLSYMS_PARAMINFO)) {
+		kunit_skip(test, "CONFIG_KALLSYMS_PARAMINFO not enabled");
+		return;
+	}
+
+	KUNIT_EXPECT_GT(test, paraminfo_num_funcs, 0U);
+	kunit_info(test, "paraminfo: %u functions in table\n",
+		   paraminfo_num_funcs);
+}
+
+static struct kunit_case paraminfo_test_cases[] = {
+	KUNIT_CASE(test_paraminfo_tables_present),
+	KUNIT_CASE(test_paraminfo_two_args),
+	KUNIT_CASE(test_paraminfo_ptr_arg),
+	KUNIT_CASE(test_paraminfo_many_args),
+	KUNIT_CASE(test_paraminfo_no_args),
+	KUNIT_CASE(test_paraminfo_bogus_addr),
+	{}
+};
+
+static struct kunit_suite paraminfo_test_suite = {
+	.name = "paraminfo",
+	.test_cases = paraminfo_test_cases,
+};
+
+kunit_test_suite(paraminfo_test_suite);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("KUnit tests for kallsyms paraminfo");
diff --git a/scripts/Makefile b/scripts/Makefile
index ffe89875b3295..f681b94c6d9e7 100644
--- a/scripts/Makefile
+++ b/scripts/Makefile
@@ -5,6 +5,7 @@
 
 hostprogs-always-$(CONFIG_KALLSYMS)			+= kallsyms
 hostprogs-always-$(CONFIG_KALLSYMS_LINEINFO)		+= gen_lineinfo
+hostprogs-always-$(CONFIG_KALLSYMS_PARAMINFO)		+= gen_paraminfo
 hostprogs-always-$(BUILD_C_RECORDMCOUNT)		+= recordmcount
 hostprogs-always-$(CONFIG_BUILDTIME_TABLE_SORT)		+= sorttable
 hostprogs-always-$(CONFIG_ASN1)				+= asn1_compiler
@@ -39,6 +40,8 @@ HOSTCFLAGS_sign-file.o = $(shell $(HOSTPKG_CONFIG) --cflags libcrypto 2> /dev/nu
 HOSTLDLIBS_sign-file = $(shell $(HOSTPKG_CONFIG) --libs libcrypto 2> /dev/null || echo -lcrypto)
 HOSTCFLAGS_gen_lineinfo.o = $(shell $(HOSTPKG_CONFIG) --cflags libdw 2> /dev/null)
 HOSTLDLIBS_gen_lineinfo = $(shell $(HOSTPKG_CONFIG) --libs libdw 2> /dev/null || echo -ldw -lelf -lz)
+HOSTCFLAGS_gen_paraminfo.o = $(shell $(HOSTPKG_CONFIG) --cflags libdw 2> /dev/null)
+HOSTLDLIBS_gen_paraminfo = $(shell $(HOSTPKG_CONFIG) --libs libdw 2> /dev/null || echo -ldw -lelf -lz)
 
 ifdef CONFIG_UNWINDER_ORC
 ifeq ($(ARCH),x86_64)
diff --git a/scripts/empty_paraminfo.S b/scripts/empty_paraminfo.S
new file mode 100644
index 0000000000000..a0e39dbfbd3cb
--- /dev/null
+++ b/scripts/empty_paraminfo.S
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Empty paraminfo stub for the initial vmlinux link.
+ * The real paraminfo is generated from .tmp_vmlinux1 by gen_paraminfo.
+ */
+	.section .rodata, "a"
+	.globl paraminfo_num_funcs
+	.balign 4
+paraminfo_num_funcs:
+	.long 0
+	.globl paraminfo_func_addrs
+paraminfo_func_addrs:
+	.globl paraminfo_func_offsets
+paraminfo_func_offsets:
+	.globl paraminfo_func_data
+paraminfo_func_data:
+	.globl paraminfo_strings
+paraminfo_strings:
diff --git a/scripts/gen_paraminfo.c b/scripts/gen_paraminfo.c
new file mode 100644
index 0000000000000..ea1d23f3ddd9a
--- /dev/null
+++ b/scripts/gen_paraminfo.c
@@ -0,0 +1,549 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * gen_paraminfo.c - Generate function parameter info tables from DWARF
+ *
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Reads DWARF .debug_info from a vmlinux ELF file and outputs an assembly
+ * file containing function parameter name/type tables that the kernel uses
+ * to annotate oops/WARN dumps with typed parameter values.
+ *
+ * Requires libdw from elfutils.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <elfutils/libdw.h>
+#include <dwarf.h>
+#include <elf.h>
+#include <gelf.h>
+#include <limits.h>
+
+/* Maximum register-passed parameters on x86-64 */
+#define MAX_PARAMS 6
+
+/* Maximum length for a type name string */
+#define MAX_TYPE_LEN 64
+
+struct param_info {
+	char name[64];
+	char type[MAX_TYPE_LEN];
+};
+
+struct func_entry {
+	unsigned int offset;		/* offset from _text */
+	unsigned int num_params;
+	struct param_info params[MAX_PARAMS];
+};
+
+static struct func_entry *funcs;
+static unsigned int num_funcs;
+static unsigned int funcs_capacity;
+
+/*
+ * String table for parameter names and type names.
+ * Deduplicated via a hash table.
+ */
+struct str_entry {
+	char *str;
+	unsigned int offset;	/* byte offset in concatenated output */
+};
+
+static struct str_entry *strtab;
+static unsigned int num_strings;
+static unsigned int strtab_capacity;
+static unsigned int strtab_total_size;
+
+#define STR_HASH_BITS 14
+#define STR_HASH_SIZE (1 << STR_HASH_BITS)
+
+struct str_hash_entry {
+	const char *str;
+	unsigned int idx;	/* index into strtab[] */
+};
+
+static struct str_hash_entry str_hash[STR_HASH_SIZE];
+
+static unsigned int hash_str(const char *s)
+{
+	unsigned int h = 5381;
+
+	for (; *s; s++)
+		h = h * 33 + (unsigned char)*s;
+	return h & (STR_HASH_SIZE - 1);
+}
+
+static unsigned int find_or_add_string(const char *s)
+{
+	unsigned int h = hash_str(s);
+
+	while (str_hash[h].str) {
+		if (!strcmp(str_hash[h].str, s))
+			return str_hash[h].idx;
+		h = (h + 1) & (STR_HASH_SIZE - 1);
+	}
+
+	if (num_strings >= strtab_capacity) {
+		strtab_capacity = strtab_capacity ? strtab_capacity * 2 : 8192;
+		strtab = realloc(strtab, strtab_capacity * sizeof(*strtab));
+		if (!strtab) {
+			fprintf(stderr, "out of memory\n");
+			exit(1);
+		}
+	}
+
+	strtab[num_strings].str = strdup(s);
+	strtab[num_strings].offset = strtab_total_size;
+	strtab_total_size += strlen(s) + 1;
+
+	str_hash[h].str = strtab[num_strings].str;
+	str_hash[h].idx = num_strings;
+
+	num_strings++;
+	return num_strings - 1;
+}
+
+static void add_func(struct func_entry *f)
+{
+	if (num_funcs >= funcs_capacity) {
+		funcs_capacity = funcs_capacity ? funcs_capacity * 2 : 16384;
+		funcs = realloc(funcs, funcs_capacity * sizeof(*funcs));
+		if (!funcs) {
+			fprintf(stderr, "out of memory\n");
+			exit(1);
+		}
+	}
+	funcs[num_funcs++] = *f;
+}
+
+/*
+ * Build a human-readable type name string from a DWARF type DIE.
+ * Follows the type chain (pointers, const, etc.) to produce strings like:
+ *   "struct file *", "const char *", "unsigned long", "void *"
+ */
+static void build_type_name(Dwarf_Die *type_die, char *buf, size_t bufsz)
+{
+	Dwarf_Die child;
+	Dwarf_Attribute attr;
+	const char *name;
+	int tag;
+
+	if (!type_die) {
+		snprintf(buf, bufsz, "void");
+		return;
+	}
+
+	tag = dwarf_tag(type_die);
+
+	switch (tag) {
+	case DW_TAG_base_type:
+		name = dwarf_diename(type_die);
+		snprintf(buf, bufsz, "%s", name ? name : "?");
+		break;
+
+	case DW_TAG_pointer_type:
+		if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+		    dwarf_formref_die(&attr, &child)) {
+			build_type_name(&child, buf, bufsz);
+			if (strlen(buf) + 3 < bufsz)
+				strcat(buf, " *");
+		} else {
+			snprintf(buf, bufsz, "void *");
+		}
+		break;
+
+	case DW_TAG_const_type:
+		if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+		    dwarf_formref_die(&attr, &child)) {
+			char tmp[MAX_TYPE_LEN - 10];
+
+			build_type_name(&child, tmp, sizeof(tmp));
+			snprintf(buf, bufsz, "const %s", tmp);
+		} else {
+			snprintf(buf, bufsz, "const void");
+		}
+		break;
+
+	case DW_TAG_volatile_type:
+		if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+		    dwarf_formref_die(&attr, &child)) {
+			char tmp[MAX_TYPE_LEN - 10];
+
+			build_type_name(&child, tmp, sizeof(tmp));
+			snprintf(buf, bufsz, "volatile %s", tmp);
+		} else {
+			snprintf(buf, bufsz, "volatile void");
+		}
+		break;
+
+	case DW_TAG_restrict_type:
+		if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+		    dwarf_formref_die(&attr, &child)) {
+			build_type_name(&child, buf, bufsz);
+		} else {
+			snprintf(buf, bufsz, "void");
+		}
+		break;
+
+	case DW_TAG_typedef:
+		name = dwarf_diename(type_die);
+		if (name) {
+			snprintf(buf, bufsz, "%s", name);
+		} else if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+			   dwarf_formref_die(&attr, &child)) {
+			build_type_name(&child, buf, bufsz);
+		} else {
+			snprintf(buf, bufsz, "?");
+		}
+		break;
+
+	case DW_TAG_structure_type:
+		name = dwarf_diename(type_die);
+		snprintf(buf, bufsz, "struct %s", name ? name : "(anon)");
+		break;
+
+	case DW_TAG_union_type:
+		name = dwarf_diename(type_die);
+		snprintf(buf, bufsz, "union %s", name ? name : "(anon)");
+		break;
+
+	case DW_TAG_enumeration_type:
+		name = dwarf_diename(type_die);
+		snprintf(buf, bufsz, "enum %s", name ? name : "(anon)");
+		break;
+
+	case DW_TAG_array_type:
+		if (dwarf_attr(type_die, DW_AT_type, &attr) &&
+		    dwarf_formref_die(&attr, &child)) {
+			build_type_name(&child, buf, bufsz);
+			if (strlen(buf) + 3 < bufsz)
+				strcat(buf, "[]");
+		} else {
+			snprintf(buf, bufsz, "?[]");
+		}
+		break;
+
+	case DW_TAG_subroutine_type:
+		snprintf(buf, bufsz, "func_ptr");
+		break;
+
+	default:
+		snprintf(buf, bufsz, "?");
+		break;
+	}
+}
+
+static unsigned long long find_text_addr(Elf *elf)
+{
+	size_t nsyms, i;
+	Elf_Scn *scn = NULL;
+	GElf_Shdr shdr;
+
+	while ((scn = elf_nextscn(elf, scn)) != NULL) {
+		Elf_Data *data;
+
+		if (!gelf_getshdr(scn, &shdr))
+			continue;
+		if (shdr.sh_type != SHT_SYMTAB)
+			continue;
+
+		data = elf_getdata(scn, NULL);
+		if (!data)
+			continue;
+
+		nsyms = shdr.sh_size / shdr.sh_entsize;
+		for (i = 0; i < nsyms; i++) {
+			GElf_Sym sym;
+			const char *name;
+
+			if (!gelf_getsym(data, i, &sym))
+				continue;
+			name = elf_strptr(elf, shdr.sh_link, sym.st_name);
+			if (name && !strcmp(name, "_text"))
+				return sym.st_value;
+		}
+	}
+
+	fprintf(stderr, "Cannot find _text symbol\n");
+	exit(1);
+}
+
+static int compare_funcs(const void *a, const void *b)
+{
+	const struct func_entry *fa = a;
+	const struct func_entry *fb = b;
+
+	if (fa->offset < fb->offset)
+		return -1;
+	if (fa->offset > fb->offset)
+		return 1;
+	return 0;
+}
+
+static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
+{
+	Dwarf_Off off = 0, next_off;
+	size_t hdr_size;
+
+	while (dwarf_nextcu(dwarf, off, &next_off, &hdr_size,
+			    NULL, NULL, NULL) == 0) {
+		Dwarf_Die cudie, child;
+
+		if (!dwarf_offdie(dwarf, off + hdr_size, &cudie))
+			goto next;
+
+		if (dwarf_child(&cudie, &child) != 0)
+			goto next;
+
+		do {
+			Dwarf_Die param;
+			Dwarf_Attribute attr;
+			Dwarf_Addr low_pc;
+			struct func_entry func;
+			int tag;
+
+			tag = dwarf_tag(&child);
+			if (tag != DW_TAG_subprogram)
+				continue;
+
+			/* Skip declarations (no body) */
+			if (dwarf_attr(&child, DW_AT_declaration, &attr))
+				continue;
+
+			/* Get function start address */
+			if (dwarf_lowpc(&child, &low_pc) != 0)
+				continue;
+
+			if (low_pc < text_addr)
+				continue;
+
+			{
+				unsigned long long raw_offset = low_pc - text_addr;
+
+				if (raw_offset > UINT_MAX)
+					continue;
+				func.offset = (unsigned int)raw_offset;
+			}
+
+			/* Iterate formal parameters */
+			func.num_params = 0;
+			if (dwarf_child(&child, &param) == 0) {
+				do {
+					Dwarf_Die type_die;
+					const char *pname;
+
+					if (dwarf_tag(&param) != DW_TAG_formal_parameter)
+						continue;
+					if (func.num_params >= MAX_PARAMS)
+						break;
+
+					pname = dwarf_diename(&param);
+					if (!pname)
+						pname = "?";
+
+					snprintf(func.params[func.num_params].name,
+						 sizeof(func.params[0].name),
+						 "%s", pname);
+
+					/* Resolve type */
+					if (dwarf_attr(&param, DW_AT_type, &attr) &&
+					    dwarf_formref_die(&attr, &type_die)) {
+						build_type_name(&type_die,
+								func.params[func.num_params].type,
+								MAX_TYPE_LEN);
+					} else {
+						snprintf(func.params[func.num_params].type,
+							 MAX_TYPE_LEN, "?");
+					}
+
+					func.num_params++;
+				} while (dwarf_siblingof(&param, &param) == 0);
+			}
+
+			/* Skip functions with no parameters */
+			if (func.num_params == 0)
+				continue;
+
+			add_func(&func);
+		} while (dwarf_siblingof(&child, &child) == 0);
+next:
+		off = next_off;
+	}
+}
+
+static void deduplicate(void)
+{
+	unsigned int i, j;
+
+	if (num_funcs < 2)
+		return;
+
+	/* Sort by offset */
+	qsort(funcs, num_funcs, sizeof(*funcs), compare_funcs);
+
+	/* Remove duplicates (same offset — keep first) */
+	j = 0;
+	for (i = 1; i < num_funcs; i++) {
+		if (funcs[i].offset == funcs[j].offset)
+			continue;
+		j++;
+		if (j != i)
+			funcs[j] = funcs[i];
+	}
+	num_funcs = j + 1;
+}
+
+static void print_escaped_asciz(const char *s)
+{
+	printf("\t.asciz \"");
+	for (; *s; s++) {
+		if (*s == '"' || *s == '\\')
+			putchar('\\');
+		putchar(*s);
+	}
+	printf("\"\n");
+}
+
+static void output_assembly(void)
+{
+	unsigned int i, j;
+
+	printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
+	printf("/*\n");
+	printf(" * Automatically generated by scripts/gen_paraminfo\n");
+	printf(" * Do not edit.\n");
+	printf(" */\n\n");
+
+	printf("\t.section .rodata, \"a\"\n\n");
+
+	/* Number of functions */
+	printf("\t.globl paraminfo_num_funcs\n");
+	printf("\t.balign 4\n");
+	printf("paraminfo_num_funcs:\n");
+	printf("\t.long %u\n\n", num_funcs);
+
+	/* Function address offsets (sorted, for binary search) */
+	printf("\t.globl paraminfo_func_addrs\n");
+	printf("\t.balign 4\n");
+	printf("paraminfo_func_addrs:\n");
+	for (i = 0; i < num_funcs; i++)
+		printf("\t.long 0x%x\n", funcs[i].offset);
+	printf("\n");
+
+	/*
+	 * Function data offsets — byte offset into paraminfo_func_data
+	 * for each function's parameter list.
+	 */
+	printf("\t.globl paraminfo_func_offsets\n");
+	printf("\t.balign 4\n");
+	printf("paraminfo_func_offsets:\n");
+	for (i = 0; i < num_funcs; i++)
+		printf("\t.long .Lfunc_%u - paraminfo_func_data\n", i);
+	printf("\n");
+
+	/*
+	 * Register strings in the string table and build the func data.
+	 * Func data format per function:
+	 *   u8  num_params
+	 *   For each param:
+	 *     u32 name_str_offset
+	 *     u32 type_str_offset
+	 */
+	/* First pass: register all strings */
+	for (i = 0; i < num_funcs; i++) {
+		for (j = 0; j < funcs[i].num_params; j++) {
+			find_or_add_string(funcs[i].params[j].name);
+			find_or_add_string(funcs[i].params[j].type);
+		}
+	}
+
+	/* Function parameter data */
+	printf("\t.globl paraminfo_func_data\n");
+	printf("paraminfo_func_data:\n");
+	for (i = 0; i < num_funcs; i++) {
+		printf(".Lfunc_%u:\n", i);
+		printf("\t.byte %u\n", funcs[i].num_params);
+		for (j = 0; j < funcs[i].num_params; j++) {
+			unsigned int name_idx = find_or_add_string(funcs[i].params[j].name);
+			unsigned int type_idx = find_or_add_string(funcs[i].params[j].type);
+
+			printf("\t.long %u, %u\n",
+			       strtab[name_idx].offset,
+			       strtab[type_idx].offset);
+		}
+	}
+	printf("\n");
+
+	/* String table */
+	printf("\t.globl paraminfo_strings\n");
+	printf("paraminfo_strings:\n");
+	for (i = 0; i < num_strings; i++)
+		print_escaped_asciz(strtab[i].str);
+	printf("\n");
+}
+
+int main(int argc, char *argv[])
+{
+	int fd;
+	Elf *elf;
+	Dwarf *dwarf;
+	unsigned long long text_addr;
+
+	if (argc != 2) {
+		fprintf(stderr, "Usage: %s <vmlinux ELF>\n", argv[0]);
+		return 1;
+	}
+
+	fd = open(argv[1], O_RDONLY);
+	if (fd < 0) {
+		fprintf(stderr, "Cannot open %s: %s\n", argv[1],
+			strerror(errno));
+		return 1;
+	}
+
+	elf_version(EV_CURRENT);
+	elf = elf_begin(fd, ELF_C_READ, NULL);
+	if (!elf) {
+		fprintf(stderr, "elf_begin failed: %s\n",
+			elf_errmsg(elf_errno()));
+		close(fd);
+		return 1;
+	}
+
+	text_addr = find_text_addr(elf);
+
+	dwarf = dwarf_begin_elf(elf, DWARF_C_READ, NULL);
+	if (!dwarf) {
+		fprintf(stderr, "dwarf_begin_elf failed: %s\n",
+			dwarf_errmsg(dwarf_errno()));
+		fprintf(stderr, "Is %s built with CONFIG_DEBUG_INFO?\n",
+			argv[1]);
+		elf_end(elf);
+		close(fd);
+		return 1;
+	}
+
+	process_dwarf(dwarf, text_addr);
+	deduplicate();
+
+	fprintf(stderr, "paraminfo: %u functions, %u strings\n",
+		num_funcs, num_strings);
+
+	output_assembly();
+
+	dwarf_end(dwarf);
+	elf_end(elf);
+	close(fd);
+
+	/* Cleanup */
+	free(funcs);
+	for (unsigned int i = 0; i < num_strings; i++)
+		free(strtab[i].str);
+	free(strtab);
+
+	return 0;
+}
diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh
index 39ca44fbb259b..d41b31dde4ef9 100755
--- a/scripts/link-vmlinux.sh
+++ b/scripts/link-vmlinux.sh
@@ -103,7 +103,7 @@ vmlinux_link()
 	${ld} ${ldflags} -o ${output}					\
 		${wl}--whole-archive ${objs} ${wl}--no-whole-archive	\
 		${wl}--start-group ${libs} ${wl}--end-group		\
-		${kallsymso} ${lineinfo_o} ${btf_vmlinux_bin_o} ${arch_vmlinux_o} ${ldlibs}
+		${kallsymso} ${lineinfo_o} ${paraminfo_o} ${btf_vmlinux_bin_o} ${arch_vmlinux_o} ${ldlibs}
 }
 
 # Create ${2}.o file with all symbols from the ${1} object file
@@ -149,6 +149,26 @@ gen_lineinfo()
 	lineinfo_o=.tmp_lineinfo.o
 }
 
+# Generate paraminfo tables from DWARF debug info in a temporary vmlinux.
+# ${1} - temporary vmlinux with debug info
+# Output: sets paraminfo_o to the generated .o file
+gen_paraminfo()
+{
+	info PARAMINFO .tmp_paraminfo.S
+	if ! scripts/gen_paraminfo "${1}" > .tmp_paraminfo.S; then
+		echo >&2 "Failed to generate paraminfo from ${1}"
+		echo >&2 "Try to disable CONFIG_KALLSYMS_PARAMINFO"
+		exit 1
+	fi
+
+	info AS .tmp_paraminfo.o
+	${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+	      ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \
+	      -c -o .tmp_paraminfo.o .tmp_paraminfo.S
+
+	paraminfo_o=.tmp_paraminfo.o
+}
+
 # Perform kallsyms for the given temporary vmlinux.
 sysmap_and_kallsyms()
 {
@@ -176,6 +196,7 @@ cleanup()
 {
 	rm -f .btf.*
 	rm -f .tmp_lineinfo.*
+	rm -f .tmp_paraminfo.*
 	rm -f .tmp_vmlinux.nm-sort
 	rm -f System.map
 	rm -f vmlinux
@@ -205,6 +226,7 @@ btf_vmlinux_bin_o=
 btfids_vmlinux=
 kallsymso=
 lineinfo_o=
+paraminfo_o=
 strip_debug=
 generate_map=
 
@@ -229,12 +251,22 @@ if is_enabled CONFIG_KALLSYMS_LINEINFO; then
 	lineinfo_o=.tmp_lineinfo.o
 fi
 
+if is_enabled CONFIG_KALLSYMS_PARAMINFO; then
+	# Assemble an empty paraminfo stub for the initial link.
+	# The real paraminfo is generated from .tmp_vmlinux1 by gen_paraminfo.
+	${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+	      ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \
+	      -c -o .tmp_paraminfo.o "${srctree}/scripts/empty_paraminfo.S"
+	paraminfo_o=.tmp_paraminfo.o
+fi
+
 if is_enabled CONFIG_KALLSYMS || is_enabled CONFIG_DEBUG_INFO_BTF; then
 
-	# The kallsyms linking does not need debug symbols, but BTF and
-	# lineinfo generation do.
+	# The kallsyms linking does not need debug symbols, but BTF,
+	# lineinfo and paraminfo generation do.
 	if ! is_enabled CONFIG_DEBUG_INFO_BTF &&
-	   ! is_enabled CONFIG_KALLSYMS_LINEINFO; then
+	   ! is_enabled CONFIG_KALLSYMS_LINEINFO &&
+	   ! is_enabled CONFIG_KALLSYMS_PARAMINFO; then
 		strip_debug=1
 	fi
 
@@ -256,6 +288,10 @@ if is_enabled CONFIG_KALLSYMS_LINEINFO; then
 	gen_lineinfo .tmp_vmlinux1
 fi
 
+if is_enabled CONFIG_KALLSYMS_PARAMINFO; then
+	gen_paraminfo .tmp_vmlinux1
+fi
+
 if is_enabled CONFIG_KALLSYMS; then
 
 	# kallsyms support
-- 
2.51.0


^ permalink raw reply related

* [PATCH 0/2] kallsyms: show typed function parameters in oops/WARN dumps
From: Sasha Levin @ 2026-03-23 16:48 UTC (permalink / raw)
  To: Andrew Morton, Masahiro Yamada, Nathan Chancellor, Nicolas Schier
  Cc: Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen,
	H. Peter Anvin, Peter Zijlstra, Josh Poimboeuf, Petr Mladek,
	Alexei Starovoitov, Jonathan Corbet, David Gow, Kees Cook,
	Greg KH, Luis Chamberlain, Steven Rostedt, Helge Deller,
	Randy Dunlap, Geert Uytterhoeven, Juergen Gross, James Bottomley,
	Alexey Dobriyan, Vlastimil Babka, Laurent Pinchart, Petr Pavlu,
	x86, linux-kernel, linux-kbuild, linux-doc, linux-modules, bpf,
	Sasha Levin

Building on the lineinfo series, this adds typed function parameter
display to oops and WARN dumps.  A build-time tool extracts parameter
names and types from DWARF, and the kernel maps pt_regs to the calling
convention at crash time.  When BTF is available, struct pointer
parameters are dereferenced and their members displayed.

Example output from a WARN in a function receiving struct new_utsname *
(kernel version info) and struct file * parameters:

 ------------[ cut here ]------------
 WARNING: drivers/tty/sysrq.c:1209 at demo_crash+0xf/0x20 (drivers/tty/sysrq.c:1209)
 CPU: 2 UID: 0 PID: 323 Comm: bash
 RIP: 0010:demo_crash+0xf/0x20 (drivers/tty/sysrq.c:1209)
 ...
 RDI: ffffffffb8ca8d00
 RSI: ffffa0a3c250acc0
 ...
 Function parameters (paraminfo_demo_crash):
  uts      (struct new_utsname *) = 0xffffffffb8ca8d00
   .sysname = "Linux"                        .nodename = "localhost"
   .release = "7.0.0-rc2-00006-g3190..."     .version = "#45 SMP PRE"
  file     (struct file *       ) = 0xffffa0a3c250acc0
   .f_mode = (fmode_t)67993630               .f_op = (struct file_operations *)0xffffffffb7237620
   .f_flags = (unsigned int)32769            .f_cred = (struct cred *)0xffffa0a3c2e06a80
   .dentry = (struct dentry *)0xffffa0a3c0978cc0
   .prev_pos = (loff_t)-1
 Call Trace:
  <TASK>
  write_sysrq_trigger+0x96/0xb0 (drivers/tty/sysrq.c:1222)
  proc_reg_write+0x54/0xa0 (fs/proc/inode.c:330)
  vfs_write+0xc9/0x480 (fs/read_write.c:686)
  ksys_write+0x6e/0xe0 (fs/read_write.c:738)
  do_syscall_64+0xe2/0x570 (arch/x86/entry/syscall_64.c:62)
  entry_SYSCALL_64_after_hwframe+0x77/0x7f (arch/x86/entry/entry_64.S:121)

Patch 1 adds the core paraminfo infrastructure (DWARF extraction,
kernel-side lookup, register-to-parameter mapping, ~1-2 MB overhead).
Patch 2 adds optional BTF-based struct rendering, gated behind
CONFIG_KALLSYMS_PARAMINFO_BTF.

Sasha Levin (2):
  kallsyms: show function parameter info in oops/WARN dumps
  kallsyms: add BTF-based deep parameter rendering in oops dumps

 .../admin-guide/kallsyms-lineinfo.rst         |  31 +
 arch/x86/kernel/dumpstack.c                   |   6 +-
 include/linux/kallsyms.h                      |   9 +
 init/Kconfig                                  |  40 ++
 kernel/Makefile                               |   1 +
 kernel/kallsyms.c                             | 182 ++++++
 kernel/kallsyms_internal.h                    |   6 +
 kernel/kallsyms_paraminfo_btf.c               | 199 ++++++
 lib/Kconfig.debug                             |  11 +
 lib/tests/Makefile                            |   3 +
 lib/tests/paraminfo_kunit.c                   | 249 ++++++++
 scripts/Makefile                              |   3 +
 scripts/empty_paraminfo.S                     |  18 +
 scripts/gen_paraminfo.c                       | 597 ++++++++++++++++++
 scripts/link-vmlinux.sh                       |  44 +-
 15 files changed, 1393 insertions(+), 6 deletions(-)
 create mode 100644 kernel/kallsyms_paraminfo_btf.c
 create mode 100644 lib/tests/paraminfo_kunit.c
 create mode 100644 scripts/empty_paraminfo.S
 create mode 100644 scripts/gen_paraminfo.c

--
2.51.0


^ permalink raw reply

* Re: [PATCH v4 0/4] kallsyms: embed source file:line info in kernel stack traces
From: Sasha Levin @ 2026-03-23 14:06 UTC (permalink / raw)
  To: Andrew Morton
  Cc: Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Petr Mladek,
	Steven Rostedt, Kees Cook, Peter Zijlstra, Thorsten Leemhuis,
	Vlastimil Babka, Helge Deller, Randy Dunlap, Laurent Pinchart,
	Vivian Wang, linux-kernel, linux-kbuild, linux-modules, linux-doc
In-Reply-To: <20260322093533.c0aab4ed9f5eef9536d14c21@linux-foundation.org>

On Sun, Mar 22, 2026 at 09:35:33AM -0700, Andrew Morton wrote:
>On Sun, 22 Mar 2026 09:15:39 -0400 Sasha Levin <sashal@kernel.org> wrote:
>
>> This series adds CONFIG_KALLSYMS_LINEINFO, which embeds source file:line
>> information directly in the kernel image so that stack traces annotate
>> every frame with the originating source location - no external tools, no
>> debug symbols at runtime, and safe to use in NMI/panic context.
>
>Sashiko review hasn't completed yet, but it has things to say:
>	https://sashiko.dev/#/patchset/20260322131543.971079-1-sashal@kernel.org

Nice! I looked at the comments, and I don't think that there are any changes
required as a result of the review. It asked good questions, but the concerns
are mainly false positives.

-- 
Thanks,
Sasha

^ permalink raw reply

* Re: [PATCH] rust: module_param: return copy from value() for Copy types
From: Andreas Hindborg @ 2026-03-23 13:23 UTC (permalink / raw)
  To: Gary Guo, Luis Chamberlain, Petr Pavlu, Daniel Gomez,
	Sami Tolvanen, Aaron Tomlin, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Alice Ryhl, Trevor Gross,
	Danilo Krummrich
  Cc: linux-modules, linux-kernel, rust-for-linux
In-Reply-To: <DHA6CNZ4W0DH.388Y1VMS61C7T@garyguo.net>

"Gary Guo" <gary@garyguo.net> writes:

> On Mon Mar 23, 2026 at 12:47 PM GMT, Andreas Hindborg wrote:
>> Rename the existing `value()` method to `value_ref()` which returns a
>> shared reference to the parameter value, and add a new `value()`
>> method on `ModuleParamAccess<T>` where `T: Copy` that returns the
>> value by copy.
>>
>> This provides a more ergonomic API for the common case where the
>> parameter type implements `Copy`, avoiding the need to explicitly
>> dereference the return value at call sites.
>>
>> Currently `value_ref()` has no in-tree callers, but it will be needed
>> when support for non-`Copy` parameter types such as arrays and
>> strings is added.
>>
>> Signed-off-by: Andreas Hindborg <a.hindborg@kernel.org>
>> ---
>> This change was suggested at [1].
>>
>> Link: https://lore.kernel.org/r/87cy13swpw.fsf@t14s.mail-host-address-is-not-set [1]
>> ---
>>  rust/kernel/module_param.rs  | 11 ++++++++++-
>>  samples/rust/rust_minimal.rs |  2 +-
>>  2 files changed, 11 insertions(+), 2 deletions(-)
>>
>> diff --git a/rust/kernel/module_param.rs b/rust/kernel/module_param.rs
>> index 6a8a7a875643..5dcfe2ba87a1 100644
>> --- a/rust/kernel/module_param.rs
>> +++ b/rust/kernel/module_param.rs
>> @@ -134,7 +134,7 @@ pub const fn new(default: T) -> Self {
>>      /// Get a shared reference to the parameter value.
>>      // Note: When sysfs access to parameters are enabled, we have to pass in a
>>      // held lock guard here.
>> -    pub fn value(&self) -> &T {
>> +    pub fn value_ref(&self) -> &T {
>>          self.value.as_ref().unwrap_or(&self.default)
>>      }
>>
>> @@ -146,6 +146,15 @@ pub const fn as_void_ptr(&self) -> *mut c_void {
>>      }
>>  }
>>
>> +impl<T: Copy> ModuleParamAccess<T> {
>> +    /// Get a copy of the parameter value.
>> +    // Note: When sysfs access to parameters are enabled, we have to pass in a
>> +    // held lock guard here.
>> +    pub fn value(&self) -> T {
>
> It's better to keep this close to `value_ref` in the same impl block. The `T:
> Copy` bound doesn't need to be on the impl block, it can be on the item itself
> with
>
>     pub fn value(&self) -> T where T: Copy

Cool, I'll do that.


Best regards,
Andreas Hindborg




^ permalink raw reply

* Re: [PATCH] module/kallsyms: sort function symbols and use binary search
From: Petr Pavlu @ 2026-03-23 13:06 UTC (permalink / raw)
  To: Stanislaw Gruszka
  Cc: linux-modules, Sami Tolvanen, Luis Chamberlain, linux-kernel,
	linux-trace-kernel, live-patching, Daniel Gomez, Aaron Tomlin,
	Steven Rostedt, Masami Hiramatsu, Jordan Rome, Viktor Malik
In-Reply-To: <20260317110423.45481-1-stf_xl@wp.pl>

On 3/17/26 12:04 PM, Stanislaw Gruszka wrote:
> Module symbol lookup via find_kallsyms_symbol() performs a linear scan
> over the entire symtab when resolving an address. The number of symbols
> in module symtabs has grown over the years, largely due to additional
> metadata in non-standard sections, making this lookup very slow.
> 
> Improve this by separating function symbols during module load, placing
> them at the beginning of the symtab, sorting them by address, and using
> binary search when resolving addresses in module text.

Doesn't considering only function symbols break the expected behavior
with CONFIG_KALLSYMS_ALL=y. For instance, when using kdb, is it still
able to see all symbols in a module? The module loader should be remain
consistent with the main kallsyms code regarding which symbols can be
looked up.

> 
> This also should improve times for linear symbol name lookups, as valid
> function symbols are now located at the beginning of the symtab.
> 
> The cost of sorting is small relative to module load time. In repeated
> module load tests [1], depending on .config options, this change
> increases load time between 2% and 4%. With cold caches, the difference
> is not measurable, as memory access latency dominates.
> 
> The sorting theoretically could be done in compile time, but much more
> complicated as we would have to simulate kernel addresses resolution
> for symbols, and then correct relocation entries. That would be risky
> if get out of sync.
> 
> The improvement can be observed when listing ftrace filter functions:
> 
> root@nano:~# time cat /sys/kernel/tracing/available_filter_functions | wc -l
> 74908
> 
> real	0m1.315s
> user	0m0.000s
> sys	0m1.312s
> 
> After:
> 
> root@nano:~# time cat /sys/kernel/tracing/available_filter_functions | wc -l
> 74911
> 
> real	0m0.167s
> user	0m0.004s
> sys	0m0.175s
> 
> (there are three more symbols introduced by the patch)

This looks as a reasonable improvement.

> 
> For livepatch modules, the symtab layout is preserved and the existing
> linear search is used. For this case, it should be possible to keep
> the original ELF symtab instead of copying it 1:1, but that is outside
> the scope of this patch.

Livepatch modules are already handled specially by the kallsyms module
code so excluding them from this optimization is probably ok.

However, it might be worth revisiting this exception. I believe that
livepatch support requires the original symbol table for relocations to
remain usable. It might make sense to investigate whether updating the
relocation data with the adjusted symbol indexes would be sensible.

-- 
Thanks,
Petr

^ permalink raw reply

* Re: [PATCH] rust: module_param: return copy from value() for Copy types
From: Gary Guo @ 2026-03-23 12:52 UTC (permalink / raw)
  To: Andreas Hindborg, Luis Chamberlain, Petr Pavlu, Daniel Gomez,
	Sami Tolvanen, Aaron Tomlin, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Alice Ryhl, Trevor Gross,
	Danilo Krummrich
  Cc: linux-modules, linux-kernel, rust-for-linux
In-Reply-To: <20260323-module-value-ref-v1-1-32507e1085f1@kernel.org>

On Mon Mar 23, 2026 at 12:47 PM GMT, Andreas Hindborg wrote:
> Rename the existing `value()` method to `value_ref()` which returns a
> shared reference to the parameter value, and add a new `value()`
> method on `ModuleParamAccess<T>` where `T: Copy` that returns the
> value by copy.
>
> This provides a more ergonomic API for the common case where the
> parameter type implements `Copy`, avoiding the need to explicitly
> dereference the return value at call sites.
>
> Currently `value_ref()` has no in-tree callers, but it will be needed
> when support for non-`Copy` parameter types such as arrays and
> strings is added.
>
> Signed-off-by: Andreas Hindborg <a.hindborg@kernel.org>
> ---
> This change was suggested at [1].
>
> Link: https://lore.kernel.org/r/87cy13swpw.fsf@t14s.mail-host-address-is-not-set [1]
> ---
>  rust/kernel/module_param.rs  | 11 ++++++++++-
>  samples/rust/rust_minimal.rs |  2 +-
>  2 files changed, 11 insertions(+), 2 deletions(-)
>
> diff --git a/rust/kernel/module_param.rs b/rust/kernel/module_param.rs
> index 6a8a7a875643..5dcfe2ba87a1 100644
> --- a/rust/kernel/module_param.rs
> +++ b/rust/kernel/module_param.rs
> @@ -134,7 +134,7 @@ pub const fn new(default: T) -> Self {
>      /// Get a shared reference to the parameter value.
>      // Note: When sysfs access to parameters are enabled, we have to pass in a
>      // held lock guard here.
> -    pub fn value(&self) -> &T {
> +    pub fn value_ref(&self) -> &T {
>          self.value.as_ref().unwrap_or(&self.default)
>      }
>  
> @@ -146,6 +146,15 @@ pub const fn as_void_ptr(&self) -> *mut c_void {
>      }
>  }
>  
> +impl<T: Copy> ModuleParamAccess<T> {
> +    /// Get a copy of the parameter value.
> +    // Note: When sysfs access to parameters are enabled, we have to pass in a
> +    // held lock guard here.
> +    pub fn value(&self) -> T {

It's better to keep this close to `value_ref` in the same impl block. The `T:
Copy` bound doesn't need to be on the impl block, it can be on the item itself
with

    pub fn value(&self) -> T where T: Copy

Best,
Gary

> +        self.value.copy().unwrap_or(self.default)
> +    }
> +}
> +
>  #[doc(hidden)]
>  /// Generate a static [`kernel_param_ops`](srctree/include/linux/moduleparam.h) struct.
>  ///
> diff --git a/samples/rust/rust_minimal.rs b/samples/rust/rust_minimal.rs
> index 8eb9583571d7..60d03df6cd80 100644
> --- a/samples/rust/rust_minimal.rs
> +++ b/samples/rust/rust_minimal.rs
> @@ -28,7 +28,7 @@ fn init(_module: &'static ThisModule) -> Result<Self> {
>          pr_info!("Am I built-in? {}\n", !cfg!(MODULE));
>          pr_info!(
>              "test_parameter: {}\n",
> -            *module_parameters::test_parameter.value()
> +            module_parameters::test_parameter.value()
>          );
>  
>          let mut numbers = KVec::new();
>
> ---
> base-commit: c369299895a591d96745d6492d4888259b004a9e
> change-id: 20260323-module-value-ref-5884b5ae6b2a
>
> Best regards,


^ permalink raw reply

* Re: [PATCH] rust: module_param: return copy from value() for Copy types
From: Alice Ryhl @ 2026-03-23 12:49 UTC (permalink / raw)
  To: Andreas Hindborg
  Cc: Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen,
	Aaron Tomlin, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Trevor Gross,
	Danilo Krummrich, linux-modules, linux-kernel, rust-for-linux
In-Reply-To: <20260323-module-value-ref-v1-1-32507e1085f1@kernel.org>

On Mon, Mar 23, 2026 at 1:48 PM Andreas Hindborg <a.hindborg@kernel.org> wrote:
>
> Rename the existing `value()` method to `value_ref()` which returns a
> shared reference to the parameter value, and add a new `value()`
> method on `ModuleParamAccess<T>` where `T: Copy` that returns the
> value by copy.
>
> This provides a more ergonomic API for the common case where the
> parameter type implements `Copy`, avoiding the need to explicitly
> dereference the return value at call sites.
>
> Currently `value_ref()` has no in-tree callers, but it will be needed
> when support for non-`Copy` parameter types such as arrays and
> strings is added.
>
> Signed-off-by: Andreas Hindborg <a.hindborg@kernel.org>

Reviewed-by: Alice Ryhl <aliceryhl@google.com>

^ permalink raw reply

* [PATCH] rust: module_param: return copy from value() for Copy types
From: Andreas Hindborg @ 2026-03-23 12:47 UTC (permalink / raw)
  To: Luis Chamberlain, Petr Pavlu, Daniel Gomez, Sami Tolvanen,
	Aaron Tomlin, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Alice Ryhl, Trevor Gross,
	Danilo Krummrich
  Cc: linux-modules, linux-kernel, rust-for-linux, Andreas Hindborg

Rename the existing `value()` method to `value_ref()` which returns a
shared reference to the parameter value, and add a new `value()`
method on `ModuleParamAccess<T>` where `T: Copy` that returns the
value by copy.

This provides a more ergonomic API for the common case where the
parameter type implements `Copy`, avoiding the need to explicitly
dereference the return value at call sites.

Currently `value_ref()` has no in-tree callers, but it will be needed
when support for non-`Copy` parameter types such as arrays and
strings is added.

Signed-off-by: Andreas Hindborg <a.hindborg@kernel.org>
---
This change was suggested at [1].

Link: https://lore.kernel.org/r/87cy13swpw.fsf@t14s.mail-host-address-is-not-set [1]
---
 rust/kernel/module_param.rs  | 11 ++++++++++-
 samples/rust/rust_minimal.rs |  2 +-
 2 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/rust/kernel/module_param.rs b/rust/kernel/module_param.rs
index 6a8a7a875643..5dcfe2ba87a1 100644
--- a/rust/kernel/module_param.rs
+++ b/rust/kernel/module_param.rs
@@ -134,7 +134,7 @@ pub const fn new(default: T) -> Self {
     /// Get a shared reference to the parameter value.
     // Note: When sysfs access to parameters are enabled, we have to pass in a
     // held lock guard here.
-    pub fn value(&self) -> &T {
+    pub fn value_ref(&self) -> &T {
         self.value.as_ref().unwrap_or(&self.default)
     }
 
@@ -146,6 +146,15 @@ pub const fn as_void_ptr(&self) -> *mut c_void {
     }
 }
 
+impl<T: Copy> ModuleParamAccess<T> {
+    /// Get a copy of the parameter value.
+    // Note: When sysfs access to parameters are enabled, we have to pass in a
+    // held lock guard here.
+    pub fn value(&self) -> T {
+        self.value.copy().unwrap_or(self.default)
+    }
+}
+
 #[doc(hidden)]
 /// Generate a static [`kernel_param_ops`](srctree/include/linux/moduleparam.h) struct.
 ///
diff --git a/samples/rust/rust_minimal.rs b/samples/rust/rust_minimal.rs
index 8eb9583571d7..60d03df6cd80 100644
--- a/samples/rust/rust_minimal.rs
+++ b/samples/rust/rust_minimal.rs
@@ -28,7 +28,7 @@ fn init(_module: &'static ThisModule) -> Result<Self> {
         pr_info!("Am I built-in? {}\n", !cfg!(MODULE));
         pr_info!(
             "test_parameter: {}\n",
-            *module_parameters::test_parameter.value()
+            module_parameters::test_parameter.value()
         );
 
         let mut numbers = KVec::new();

---
base-commit: c369299895a591d96745d6492d4888259b004a9e
change-id: 20260323-module-value-ref-5884b5ae6b2a

Best regards,
-- 
Andreas Hindborg <a.hindborg@kernel.org>



^ permalink raw reply related

* Re: [PATCH v4 0/4] kallsyms: embed source file:line info in kernel stack traces
From: Andrew Morton @ 2026-03-22 16:35 UTC (permalink / raw)
  To: Sasha Levin
  Cc: Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Petr Pavlu, Daniel Gomez, Greg KH, Petr Mladek,
	Steven Rostedt, Kees Cook, Peter Zijlstra, Thorsten Leemhuis,
	Vlastimil Babka, Helge Deller, Randy Dunlap, Laurent Pinchart,
	Vivian Wang, linux-kernel, linux-kbuild, linux-modules, linux-doc
In-Reply-To: <20260322131543.971079-1-sashal@kernel.org>

On Sun, 22 Mar 2026 09:15:39 -0400 Sasha Levin <sashal@kernel.org> wrote:

> This series adds CONFIG_KALLSYMS_LINEINFO, which embeds source file:line
> information directly in the kernel image so that stack traces annotate
> every frame with the originating source location - no external tools, no
> debug symbols at runtime, and safe to use in NMI/panic context.

Sashiko review hasn't completed yet, but it has things to say:
	https://sashiko.dev/#/patchset/20260322131543.971079-1-sashal@kernel.org

^ permalink raw reply

* [PATCH v4 4/4] kallsyms: add KUnit tests for lineinfo feature
From: Sasha Levin @ 2026-03-22 13:15 UTC (permalink / raw)
  To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, Helge Deller,
	Randy Dunlap, Laurent Pinchart, Vivian Wang, linux-kernel,
	linux-kbuild, linux-modules, linux-doc, Sasha Levin
In-Reply-To: <20260322131543.971079-1-sashal@kernel.org>

Add a KUnit test module (CONFIG_LINEINFO_KUNIT_TEST) that verifies the
kallsyms lineinfo feature produces correct source file:line annotations
in stack traces.

Export sprint_backtrace() and sprint_backtrace_build_id() as GPL symbols
so the test module can exercise the backtrace APIs.

Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
 MAINTAINERS                |   1 +
 kernel/kallsyms.c          |   2 +
 lib/Kconfig.debug          |  10 +
 lib/tests/Makefile         |   3 +
 lib/tests/lineinfo_kunit.c | 813 +++++++++++++++++++++++++++++++++++++
 5 files changed, 829 insertions(+)
 create mode 100644 lib/tests/lineinfo_kunit.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 535e992ca5a20..118711f72b874 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13733,6 +13733,7 @@ M:	Sasha Levin <sashal@kernel.org>
 S:	Maintained
 F:	Documentation/admin-guide/kallsyms-lineinfo.rst
 F:	include/linux/mod_lineinfo.h
+F:	lib/tests/lineinfo_kunit.c
 F:	scripts/gen-mod-lineinfo.sh
 F:	scripts/gen_lineinfo.c
 
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index 76e30cac3a277..e6f796d43dd70 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -625,6 +625,7 @@ int sprint_backtrace(char *buffer, unsigned long address)
 {
 	return __sprint_symbol(buffer, address, -1, 1, 0);
 }
+EXPORT_SYMBOL_GPL(sprint_backtrace);
 
 /**
  * sprint_backtrace_build_id - Look up a backtrace symbol and return it in a text buffer
@@ -645,6 +646,7 @@ int sprint_backtrace_build_id(char *buffer, unsigned long address)
 {
 	return __sprint_symbol(buffer, address, -1, 1, 1);
 }
+EXPORT_SYMBOL_GPL(sprint_backtrace_build_id);
 
 /* To avoid using get_symbol_offset for every symbol, we carry prefix along. */
 struct kallsym_iter {
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 93f356d2b3d95..688bbcb3eaa62 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -3048,6 +3048,16 @@ config LONGEST_SYM_KUNIT_TEST
 
 	  If unsure, say N.
 
+config LINEINFO_KUNIT_TEST
+	tristate "KUnit tests for kallsyms lineinfo" if !KUNIT_ALL_TESTS
+	depends on KUNIT && KALLSYMS_LINEINFO
+	default KUNIT_ALL_TESTS
+	help
+	  KUnit tests for the kallsyms source line info feature.
+	  Verifies that stack traces include correct (file.c:line) annotations.
+
+	  If unsure, say N.
+
 config HW_BREAKPOINT_KUNIT_TEST
 	bool "Test hw_breakpoint constraints accounting" if !KUNIT_ALL_TESTS
 	depends on HAVE_HW_BREAKPOINT
diff --git a/lib/tests/Makefile b/lib/tests/Makefile
index 05f74edbc62bf..c6add3b04bbd5 100644
--- a/lib/tests/Makefile
+++ b/lib/tests/Makefile
@@ -36,6 +36,9 @@ obj-$(CONFIG_LIVEUPDATE_TEST) += liveupdate.o
 CFLAGS_longest_symbol_kunit.o += $(call cc-disable-warning, missing-prototypes)
 obj-$(CONFIG_LONGEST_SYM_KUNIT_TEST) += longest_symbol_kunit.o
 
+CFLAGS_lineinfo_kunit.o += $(call cc-option,-fno-inline-functions-called-once)
+obj-$(CONFIG_LINEINFO_KUNIT_TEST) += lineinfo_kunit.o
+
 obj-$(CONFIG_MEMCPY_KUNIT_TEST) += memcpy_kunit.o
 obj-$(CONFIG_MIN_HEAP_KUNIT_TEST) += min_heap_kunit.o
 CFLAGS_overflow_kunit.o = $(call cc-disable-warning, tautological-constant-out-of-range-compare)
diff --git a/lib/tests/lineinfo_kunit.c b/lib/tests/lineinfo_kunit.c
new file mode 100644
index 0000000000000..81696fa0000aa
--- /dev/null
+++ b/lib/tests/lineinfo_kunit.c
@@ -0,0 +1,813 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for kallsyms lineinfo (CONFIG_KALLSYMS_LINEINFO).
+ *
+ * Copyright (c) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Verifies that sprint_symbol() and related APIs append correct
+ * " (file.c:NNN)" annotations to kernel symbol lookups.
+ *
+ * Build with: CONFIG_LINEINFO_KUNIT_TEST=m (or =y)
+ * Run with:   ./tools/testing/kunit/kunit.py run lineinfo
+ */
+
+#include <kunit/test.h>
+#include <linux/kallsyms.h>
+#include <linux/module.h>
+#include <linux/smp.h>
+#include <linux/string.h>
+#include <linux/slab.h>
+#include <linux/mod_lineinfo.h>
+
+/* --------------- helpers --------------- */
+
+static char *alloc_sym_buf(struct kunit *test)
+{
+	return kunit_kzalloc(test, KSYM_SYMBOL_LEN, GFP_KERNEL);
+}
+
+/*
+ * Return true if @buf contains a lineinfo annotation matching
+ * the pattern " (<path>:<digits>)".
+ *
+ * The path may be a full path like "lib/tests/lineinfo_kunit.c" or
+ * a shortened form from module lineinfo (e.g., just a directory name).
+ */
+static bool has_lineinfo(const char *buf)
+{
+	const char *p, *colon, *end;
+
+	p = strstr(buf, " (");
+	if (!p)
+		return false;
+	p += 2; /* skip " (" */
+
+	colon = strchr(p, ':');
+	if (!colon || colon == p)
+		return false;
+
+	/* After colon: one or more digits then ')' */
+	end = colon + 1;
+	if (*end < '0' || *end > '9')
+		return false;
+	while (*end >= '0' && *end <= '9')
+		end++;
+	return *end == ')';
+}
+
+/*
+ * Extract line number from a lineinfo annotation.
+ * Returns 0 if not found.
+ */
+static unsigned int extract_line(const char *buf)
+{
+	const char *p, *colon;
+	unsigned int line = 0;
+
+	p = strstr(buf, " (");
+	if (!p)
+		return 0;
+
+	colon = strchr(p + 2, ':');
+	if (!colon)
+		return 0;
+
+	colon++;
+	while (*colon >= '0' && *colon <= '9') {
+		line = line * 10 + (*colon - '0');
+		colon++;
+	}
+	return line;
+}
+
+/*
+ * Check if the lineinfo annotation contains the given filename substring.
+ */
+static bool lineinfo_contains_file(const char *buf, const char *name)
+{
+	const char *p, *colon;
+
+	p = strstr(buf, " (");
+	if (!p)
+		return false;
+
+	colon = strchr(p + 2, ':');
+	if (!colon)
+		return false;
+
+	/* Search for @name between '(' and ':' */
+	return strnstr(p + 1, name, colon - p - 1) != NULL;
+}
+
+/* --------------- target functions --------------- */
+
+static noinline int lineinfo_target_normal(void)
+{
+	barrier();
+	return 42;
+}
+
+static noinline int lineinfo_target_short(void)
+{
+	barrier();
+	return 1;
+}
+
+static noinline int lineinfo_target_with_arg(int x)
+{
+	barrier();
+	return x + 1;
+}
+
+static noinline int lineinfo_target_many_lines(void)
+{
+	int a = 0;
+
+	barrier();
+	a += 1;
+	a += 2;
+	a += 3;
+	a += 4;
+	a += 5;
+	a += 6;
+	a += 7;
+	a += 8;
+	a += 9;
+	a += 10;
+	barrier();
+	return a;
+}
+
+static __always_inline int lineinfo_inline_helper(void)
+{
+	return 99;
+}
+
+static noinline int lineinfo_inline_caller(void)
+{
+	barrier();
+	return lineinfo_inline_helper();
+}
+
+/* 10-deep call chain */
+static noinline int lineinfo_chain_10(void) { barrier(); return 10; }
+static noinline int lineinfo_chain_9(void)  { barrier(); return lineinfo_chain_10(); }
+static noinline int lineinfo_chain_8(void)  { barrier(); return lineinfo_chain_9(); }
+static noinline int lineinfo_chain_7(void)  { barrier(); return lineinfo_chain_8(); }
+static noinline int lineinfo_chain_6(void)  { barrier(); return lineinfo_chain_7(); }
+static noinline int lineinfo_chain_5(void)  { barrier(); return lineinfo_chain_6(); }
+static noinline int lineinfo_chain_4(void)  { barrier(); return lineinfo_chain_5(); }
+static noinline int lineinfo_chain_3(void)  { barrier(); return lineinfo_chain_4(); }
+static noinline int lineinfo_chain_2(void)  { barrier(); return lineinfo_chain_3(); }
+static noinline int lineinfo_chain_1(void)  { barrier(); return lineinfo_chain_2(); }
+
+/* --------------- Group A: Basic lineinfo presence --------------- */
+
+static void test_normal_function(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+	sprint_symbol(buf, addr);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo in: %s", buf);
+	KUNIT_EXPECT_TRUE_MSG(test,
+			      lineinfo_contains_file(buf, "lineinfo_kunit.c"),
+			      "Wrong file in: %s", buf);
+}
+
+static void test_static_function(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_short;
+
+	sprint_symbol(buf, addr);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo in: %s", buf);
+}
+
+static void test_noinline_function(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_with_arg;
+
+	sprint_symbol(buf, addr);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo in: %s", buf);
+}
+
+static void test_inline_function(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_inline_caller;
+
+	sprint_symbol(buf, addr);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo for inline caller in: %s", buf);
+	KUNIT_EXPECT_TRUE_MSG(test,
+			      lineinfo_contains_file(buf, "lineinfo_kunit.c"),
+			      "Wrong file in: %s", buf);
+}
+
+static void test_short_function(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_short;
+
+	sprint_symbol(buf, addr);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo for short function in: %s", buf);
+}
+
+static void test_many_lines_function(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_many_lines;
+	unsigned int line;
+
+	sprint_symbol(buf, addr);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo in: %s", buf);
+	line = extract_line(buf);
+	KUNIT_EXPECT_GT_MSG(test, line, (unsigned int)0,
+			    "Line number should be > 0 in: %s", buf);
+}
+
+/* --------------- Group B: Deep call chain --------------- */
+
+typedef int (*chain_fn_t)(void);
+
+static void test_deep_call_chain(struct kunit *test)
+{
+	static const chain_fn_t chain_fns[] = {
+		lineinfo_chain_1,  lineinfo_chain_2,
+		lineinfo_chain_3,  lineinfo_chain_4,
+		lineinfo_chain_5,  lineinfo_chain_6,
+		lineinfo_chain_7,  lineinfo_chain_8,
+		lineinfo_chain_9,  lineinfo_chain_10,
+	};
+	char *buf = alloc_sym_buf(test);
+	int i, found = 0;
+
+	/* Call chain to prevent dead-code elimination */
+	KUNIT_ASSERT_EQ(test, lineinfo_chain_1(), 10);
+
+	for (i = 0; i < ARRAY_SIZE(chain_fns); i++) {
+		unsigned long addr = (unsigned long)chain_fns[i];
+
+		sprint_symbol(buf, addr);
+		if (has_lineinfo(buf))
+			found++;
+	}
+
+	/*
+	 * Not every tiny function gets DWARF line info (compiler may
+	 * omit it for very small stubs), but at least some should.
+	 */
+	KUNIT_EXPECT_GT_MSG(test, found, 0,
+			    "None of the 10 chain functions had lineinfo");
+}
+
+/* --------------- Group C: sprint_symbol API variants --------------- */
+
+static void test_sprint_symbol_format(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+	sprint_symbol(buf, addr);
+
+	/* Should contain +0x and /0x for offset/size */
+	KUNIT_EXPECT_NOT_NULL_MSG(test, strstr(buf, "+0x"),
+				  "Missing offset in: %s", buf);
+	KUNIT_EXPECT_NOT_NULL_MSG(test, strstr(buf, "/0x"),
+				  "Missing size in: %s", buf);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo in: %s", buf);
+}
+
+static void test_sprint_backtrace(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+	/* sprint_backtrace subtracts 1 internally to handle tail calls */
+	sprint_backtrace(buf, addr + 1);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo in backtrace: %s", buf);
+	KUNIT_EXPECT_TRUE_MSG(test,
+			      lineinfo_contains_file(buf, "lineinfo_kunit.c"),
+			      "Wrong file in backtrace: %s", buf);
+}
+
+static void test_sprint_backtrace_build_id(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+	sprint_backtrace_build_id(buf, addr + 1);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo in backtrace_build_id: %s", buf);
+}
+
+static void test_sprint_symbol_no_offset(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+	sprint_symbol_no_offset(buf, addr);
+	/* No "+0x" in output */
+	KUNIT_EXPECT_NULL_MSG(test, strstr(buf, "+0x"),
+			      "Unexpected offset in no_offset: %s", buf);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo in no_offset: %s", buf);
+}
+
+/* --------------- Group D: printk format specifiers --------------- */
+
+static void test_pS_format(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	void *addr = lineinfo_target_normal;
+
+	snprintf(buf, KSYM_SYMBOL_LEN, "%pS", addr);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo in %%pS: %s", buf);
+}
+
+static void test_pBb_format(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	/*
+	 * %pBb uses sprint_backtrace_build_id which subtracts 1 from the
+	 * address, so pass addr+1 to resolve back to the function.
+	 */
+	void *addr = (void *)((unsigned long)lineinfo_target_normal + 1);
+
+	snprintf(buf, KSYM_SYMBOL_LEN, "%pBb", addr);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo in %%pBb: %s", buf);
+}
+
+static void test_pSR_format(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	void *addr = lineinfo_target_normal;
+
+	snprintf(buf, KSYM_SYMBOL_LEN, "%pSR", addr);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo in %%pSR: %s", buf);
+}
+
+/* --------------- Group E: Address edge cases --------------- */
+
+static void test_symbol_start_addr(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+	sprint_symbol(buf, addr);
+	KUNIT_EXPECT_NOT_NULL_MSG(test, strstr(buf, "+0x0/"),
+				  "Expected +0x0/ at function start: %s", buf);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo at function start: %s", buf);
+}
+
+static void test_symbol_nonzero_offset(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+	/*
+	 * sprint_backtrace subtracts 1 internally.
+	 * Passing addr+2 resolves to addr+1 which is inside the function
+	 * at a non-zero offset.
+	 */
+	sprint_backtrace(buf, addr + 2);
+	KUNIT_EXPECT_TRUE_MSG(test,
+			      strnstr(buf, "lineinfo_target_normal",
+				      KSYM_SYMBOL_LEN) != NULL,
+			      "Didn't resolve to expected function: %s", buf);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo at non-zero offset: %s", buf);
+}
+
+static void test_unknown_address(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+
+	sprint_symbol(buf, 1UL);
+	/* Should be "0x1" with no lineinfo */
+	KUNIT_EXPECT_NOT_NULL_MSG(test, strstr(buf, "0x1"),
+				  "Expected hex address for bogus addr: %s", buf);
+	KUNIT_EXPECT_FALSE_MSG(test, has_lineinfo(buf),
+			       "Unexpected lineinfo for bogus addr: %s", buf);
+}
+
+static void test_kernel_function_lineinfo(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)sprint_symbol;
+
+	sprint_symbol(buf, addr);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo for sprint_symbol: %s", buf);
+	KUNIT_EXPECT_TRUE_MSG(test,
+			      lineinfo_contains_file(buf, "kallsyms.c"),
+			      "Expected kallsyms.c in: %s", buf);
+}
+
+static void test_assembly_no_lineinfo(struct kunit *test)
+{
+#if IS_BUILTIN(CONFIG_LINEINFO_KUNIT_TEST)
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)_text;
+
+	sprint_symbol(buf, addr);
+	/*
+	 * _text is typically an asm entry point with no DWARF line info.
+	 * If it has lineinfo, it's a C-based entry — skip in that case.
+	 */
+	if (has_lineinfo(buf))
+		kunit_skip(test, "_text has lineinfo (C entry?): %s", buf);
+
+	KUNIT_EXPECT_FALSE_MSG(test, has_lineinfo(buf),
+			       "Unexpected lineinfo for asm symbol: %s", buf);
+#else
+	kunit_skip(test, "_text not accessible from modules");
+#endif
+}
+
+/* --------------- Group F: Module path --------------- */
+
+static void test_module_function_lineinfo(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+	if (!IS_MODULE(CONFIG_LINEINFO_KUNIT_TEST)) {
+		kunit_skip(test, "Test only meaningful when built as module");
+		return;
+	}
+
+	sprint_symbol(buf, addr);
+	KUNIT_EXPECT_NOT_NULL_MSG(test,
+				  strstr(buf, "[lineinfo_kunit"),
+				  "Missing module name in: %s", buf);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo for module function: %s", buf);
+	KUNIT_EXPECT_TRUE_MSG(test,
+			      lineinfo_contains_file(buf, "lineinfo_kunit.c"),
+			      "Wrong file for module function: %s", buf);
+}
+
+/* --------------- Group G: Stress --------------- */
+
+struct lineinfo_stress_data {
+	unsigned long addr;
+	atomic_t failures;
+};
+
+static void lineinfo_stress_fn(void *info)
+{
+	struct lineinfo_stress_data *data = info;
+	char buf[KSYM_SYMBOL_LEN];
+	int i;
+
+	for (i = 0; i < 100; i++) {
+		sprint_symbol(buf, data->addr);
+		if (!has_lineinfo(buf))
+			atomic_inc(&data->failures);
+	}
+}
+
+static void test_concurrent_sprint_symbol(struct kunit *test)
+{
+	struct lineinfo_stress_data data;
+
+	data.addr = (unsigned long)lineinfo_target_normal;
+	atomic_set(&data.failures, 0);
+
+	on_each_cpu(lineinfo_stress_fn, &data, 1);
+
+	KUNIT_EXPECT_EQ_MSG(test, atomic_read(&data.failures), 0,
+			    "Concurrent lineinfo failures detected");
+}
+
+static void test_rapid_sprint_symbol(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_normal;
+	int i, failures = 0;
+
+	for (i = 0; i < 1000; i++) {
+		sprint_symbol(buf, addr);
+		if (!has_lineinfo(buf))
+			failures++;
+	}
+
+	KUNIT_EXPECT_EQ_MSG(test, failures, 0,
+			    "Rapid sprint_symbol failures: %d/1000", failures);
+}
+
+/* --------------- Group H: Safety and plausibility --------------- */
+
+static void test_line_number_plausible(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_normal;
+	unsigned int line;
+
+	sprint_symbol(buf, addr);
+	KUNIT_ASSERT_TRUE(test, has_lineinfo(buf));
+
+	line = extract_line(buf);
+	KUNIT_EXPECT_GT_MSG(test, line, (unsigned int)0,
+			    "Line number should be > 0");
+	KUNIT_EXPECT_LT_MSG(test, line, (unsigned int)10000,
+			    "Line number %u implausibly large for this file",
+			    line);
+}
+
+static void test_buffer_no_overflow(struct kunit *test)
+{
+	const size_t canary_size = 16;
+	char *buf;
+	int i;
+
+	buf = kunit_kzalloc(test, KSYM_SYMBOL_LEN + canary_size, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, buf);
+
+	/* Fill canary area past KSYM_SYMBOL_LEN with 0xAA */
+	memset(buf + KSYM_SYMBOL_LEN, 0xAA, canary_size);
+
+	sprint_symbol(buf, (unsigned long)lineinfo_target_normal);
+
+	/* Verify canary bytes are untouched */
+	for (i = 0; i < canary_size; i++) {
+		KUNIT_EXPECT_EQ_MSG(test,
+				    (unsigned char)buf[KSYM_SYMBOL_LEN + i],
+				    (unsigned char)0xAA,
+				    "Buffer overflow at offset %d past KSYM_SYMBOL_LEN",
+				    i);
+	}
+}
+
+static void test_dump_stack_no_crash(struct kunit *test)
+{
+	/* Just verify dump_stack() completes without panic */
+	dump_stack();
+	KUNIT_SUCCEED(test);
+}
+
+static void test_sprint_symbol_build_id(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_normal;
+
+	sprint_symbol_build_id(buf, addr);
+	KUNIT_EXPECT_TRUE_MSG(test, has_lineinfo(buf),
+			      "No lineinfo in sprint_symbol_build_id: %s", buf);
+}
+
+static void test_sleb128_edge_cases(struct kunit *test)
+{
+	u32 pos;
+	int32_t result;
+
+	/* Value 0: single byte 0x00 */
+	{
+		static const u8 data[] = { 0x00 };
+
+		pos = 0;
+		result = lineinfo_read_sleb128(data, &pos, sizeof(data));
+		KUNIT_EXPECT_EQ(test, result, (int32_t)0);
+		KUNIT_EXPECT_EQ(test, pos, (u32)1);
+	}
+
+	/* Value -1: single byte 0x7F */
+	{
+		static const u8 data[] = { 0x7f };
+
+		pos = 0;
+		result = lineinfo_read_sleb128(data, &pos, sizeof(data));
+		KUNIT_EXPECT_EQ(test, result, (int32_t)-1);
+		KUNIT_EXPECT_EQ(test, pos, (u32)1);
+	}
+
+	/* Value 1: single byte 0x01 */
+	{
+		static const u8 data[] = { 0x01 };
+
+		pos = 0;
+		result = lineinfo_read_sleb128(data, &pos, sizeof(data));
+		KUNIT_EXPECT_EQ(test, result, (int32_t)1);
+		KUNIT_EXPECT_EQ(test, pos, (u32)1);
+	}
+
+	/* Value -64: single byte 0x40 */
+	{
+		static const u8 data[] = { 0x40 };
+
+		pos = 0;
+		result = lineinfo_read_sleb128(data, &pos, sizeof(data));
+		KUNIT_EXPECT_EQ(test, result, (int32_t)-64);
+		KUNIT_EXPECT_EQ(test, pos, (u32)1);
+	}
+
+	/* Value 63: single byte 0x3F */
+	{
+		static const u8 data[] = { 0x3f };
+
+		pos = 0;
+		result = lineinfo_read_sleb128(data, &pos, sizeof(data));
+		KUNIT_EXPECT_EQ(test, result, (int32_t)63);
+		KUNIT_EXPECT_EQ(test, pos, (u32)1);
+	}
+
+	/* Value -128: two bytes 0x80 0x7F */
+	{
+		static const u8 data[] = { 0x80, 0x7f };
+
+		pos = 0;
+		result = lineinfo_read_sleb128(data, &pos, sizeof(data));
+		KUNIT_EXPECT_EQ(test, result, (int32_t)-128);
+		KUNIT_EXPECT_EQ(test, pos, (u32)2);
+	}
+}
+
+static void test_uleb128_edge_cases(struct kunit *test)
+{
+	u32 pos, result;
+
+	/* Value 0: single byte 0x00 */
+	{
+		static const u8 data[] = { 0x00 };
+
+		pos = 0;
+		result = lineinfo_read_uleb128(data, &pos, sizeof(data));
+		KUNIT_EXPECT_EQ(test, result, (u32)0);
+		KUNIT_EXPECT_EQ(test, pos, (u32)1);
+	}
+
+	/* Value 127: single byte 0x7F */
+	{
+		static const u8 data[] = { 0x7F };
+
+		pos = 0;
+		result = lineinfo_read_uleb128(data, &pos, sizeof(data));
+		KUNIT_EXPECT_EQ(test, result, (u32)127);
+		KUNIT_EXPECT_EQ(test, pos, (u32)1);
+	}
+
+	/* Value 128: two bytes 0x80 0x01 */
+	{
+		static const u8 data[] = { 0x80, 0x01 };
+
+		pos = 0;
+		result = lineinfo_read_uleb128(data, &pos, sizeof(data));
+		KUNIT_EXPECT_EQ(test, result, (u32)128);
+		KUNIT_EXPECT_EQ(test, pos, (u32)2);
+	}
+
+	/* Max u32 0xFFFFFFFF: 5 bytes */
+	{
+		static const u8 data[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0x0F };
+
+		pos = 0;
+		result = lineinfo_read_uleb128(data, &pos, sizeof(data));
+		KUNIT_EXPECT_EQ(test, result, (u32)0xFFFFFFFF);
+		KUNIT_EXPECT_EQ(test, pos, (u32)5);
+	}
+
+	/* Truncated input: pos >= end returns 0 */
+	{
+		static const u8 data[] = { 0x80 };
+
+		pos = 0;
+		result = lineinfo_read_uleb128(data, &pos, 0);
+		KUNIT_EXPECT_EQ_MSG(test, result, (u32)0,
+				    "Expected 0 for empty input");
+	}
+
+	/* Truncated mid-varint: continuation byte but end reached */
+	{
+		static const u8 data[] = { 0x80 };
+
+		pos = 0;
+		result = lineinfo_read_uleb128(data, &pos, 1);
+		KUNIT_EXPECT_EQ_MSG(test, result, (u32)0,
+				    "Expected 0 for truncated varint");
+		KUNIT_EXPECT_EQ(test, pos, (u32)1);
+	}
+}
+
+static void test_line_number_accuracy(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_normal;
+	unsigned int line;
+
+	sprint_symbol(buf, addr);
+	KUNIT_ASSERT_TRUE(test, has_lineinfo(buf));
+
+	line = extract_line(buf);
+
+	/*
+	 * lineinfo_target_normal is defined around line 103-107.
+	 * Allow wide range: KASAN instrumentation and module lineinfo
+	 * address mapping can shift the reported line significantly.
+	 */
+	KUNIT_EXPECT_GE_MSG(test, line, (unsigned int)50,
+			    "Line %u too low for lineinfo_target_normal", line);
+	KUNIT_EXPECT_LE_MSG(test, line, (unsigned int)300,
+			    "Line %u too high for lineinfo_target_normal", line);
+}
+
+static void test_many_lines_mid_function(struct kunit *test)
+{
+	char *buf = alloc_sym_buf(test);
+	unsigned long addr = (unsigned long)lineinfo_target_many_lines;
+	unsigned int line;
+	unsigned long mid_addr;
+
+	/* Get function size from sprint_symbol output */
+	sprint_symbol(buf, addr);
+	KUNIT_ASSERT_TRUE(test, has_lineinfo(buf));
+
+	/* Try an address 8 bytes into the function (past prologue) */
+	mid_addr = addr + 8;
+	sprint_symbol(buf, mid_addr);
+
+	/*
+	 * Should still resolve to lineinfo_target_many_lines.
+	 * Lineinfo should be present with a plausible line number.
+	 */
+	KUNIT_EXPECT_TRUE_MSG(test,
+			      strnstr(buf, "lineinfo_target_many_lines",
+				      KSYM_SYMBOL_LEN) != NULL,
+			      "Mid-function addr resolved to wrong symbol: %s",
+			      buf);
+	if (has_lineinfo(buf)) {
+		line = extract_line(buf);
+		KUNIT_EXPECT_GE_MSG(test, line, (unsigned int)50,
+				    "Line %u too low for mid-function", line);
+		KUNIT_EXPECT_LE_MSG(test, line, (unsigned int)700,
+				    "Line %u too high for mid-function", line);
+	}
+}
+
+/* --------------- Suite registration --------------- */
+
+static struct kunit_case lineinfo_test_cases[] = {
+	/* Group A: Basic lineinfo presence */
+	KUNIT_CASE(test_normal_function),
+	KUNIT_CASE(test_static_function),
+	KUNIT_CASE(test_noinline_function),
+	KUNIT_CASE(test_inline_function),
+	KUNIT_CASE(test_short_function),
+	KUNIT_CASE(test_many_lines_function),
+	/* Group B: Deep call chain */
+	KUNIT_CASE(test_deep_call_chain),
+	/* Group C: sprint_symbol API variants */
+	KUNIT_CASE(test_sprint_symbol_format),
+	KUNIT_CASE(test_sprint_backtrace),
+	KUNIT_CASE(test_sprint_backtrace_build_id),
+	KUNIT_CASE(test_sprint_symbol_no_offset),
+	/* Group D: printk format specifiers */
+	KUNIT_CASE(test_pS_format),
+	KUNIT_CASE(test_pBb_format),
+	KUNIT_CASE(test_pSR_format),
+	/* Group E: Address edge cases */
+	KUNIT_CASE(test_symbol_start_addr),
+	KUNIT_CASE(test_symbol_nonzero_offset),
+	KUNIT_CASE(test_unknown_address),
+	KUNIT_CASE(test_kernel_function_lineinfo),
+	KUNIT_CASE(test_assembly_no_lineinfo),
+	/* Group F: Module path */
+	KUNIT_CASE(test_module_function_lineinfo),
+	/* Group G: Stress */
+	KUNIT_CASE_SLOW(test_concurrent_sprint_symbol),
+	KUNIT_CASE_SLOW(test_rapid_sprint_symbol),
+	/* Group H: Safety and plausibility */
+	KUNIT_CASE(test_line_number_plausible),
+	KUNIT_CASE(test_buffer_no_overflow),
+	KUNIT_CASE(test_dump_stack_no_crash),
+	KUNIT_CASE(test_sprint_symbol_build_id),
+	/* Group I: Encoding/decoding and accuracy */
+	KUNIT_CASE(test_sleb128_edge_cases),
+	KUNIT_CASE(test_uleb128_edge_cases),
+	KUNIT_CASE(test_line_number_accuracy),
+	KUNIT_CASE(test_many_lines_mid_function),
+	{}
+};
+
+static struct kunit_suite lineinfo_test_suite = {
+	.name = "lineinfo",
+	.test_cases = lineinfo_test_cases,
+};
+kunit_test_suites(&lineinfo_test_suite);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("KUnit tests for kallsyms lineinfo");
+MODULE_AUTHOR("Sasha Levin");
-- 
2.51.0


^ permalink raw reply related

* [PATCH v4 3/4] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
From: Sasha Levin @ 2026-03-22 13:15 UTC (permalink / raw)
  To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, Helge Deller,
	Randy Dunlap, Laurent Pinchart, Vivian Wang, linux-kernel,
	linux-kbuild, linux-modules, linux-doc, Sasha Levin
In-Reply-To: <20260322131543.971079-1-sashal@kernel.org>

Replace the flat uncompressed parallel arrays (lineinfo_addrs[],
lineinfo_file_ids[], lineinfo_lines[]) with a block-indexed,
delta-encoded, ULEB128 varint compressed format.

The sorted address array has small deltas between consecutive entries
(typically 1-50 bytes), file IDs have high locality (delta often 0,
same file), and line numbers change slowly.  Delta-encoding followed
by ULEB128 varint compression shrinks most values from 4 bytes to 1.

Entries are grouped into blocks of 64.  A small uncompressed block
index (first addr + byte offset per block) enables O(log(N/64)) binary
search, followed by sequential decode of at most 64 varints within the
matching block.  All decode state lives on the stack -- zero
allocations, still safe for NMI/panic context.

Measured on a defconfig+debug x86_64 build (3,017,154 entries, 4,822
source files, 47,144 blocks):

  Before (flat arrays):
    lineinfo_addrs[]    12,068,616 bytes (u32 x 3.0M)
    lineinfo_file_ids[]  6,034,308 bytes (u16 x 3.0M)
    lineinfo_lines[]    12,068,616 bytes (u32 x 3.0M)
    Total:              30,171,540 bytes (28.8 MiB, 10.0 bytes/entry)

  After (block-indexed delta + ULEB128):
    lineinfo_block_addrs[]    188,576 bytes (184 KiB)
    lineinfo_block_offsets[]  188,576 bytes (184 KiB)
    lineinfo_data[]        10,926,128 bytes (10.4 MiB)
    Total:                 11,303,280 bytes (10.8 MiB, 3.7 bytes/entry)

  Savings: 18.0 MiB (2.7x reduction)

Booted in QEMU and verified with SysRq-l that annotations still work:

  default_idle+0x9/0x10 (arch/x86/kernel/process.c:767)
  default_idle_call+0x6c/0xb0 (kernel/sched/idle.c:122)
  do_idle+0x335/0x490 (kernel/sched/idle.c:191)
  cpu_startup_entry+0x4e/0x60 (kernel/sched/idle.c:429)
  rest_init+0x1aa/0x1b0 (init/main.c:760)

Suggested-by: Juergen Gross <jgross@suse.com>
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
 .../admin-guide/kallsyms-lineinfo.rst         |   7 +-
 include/linux/mod_lineinfo.h                  | 227 ++++++++++++++++--
 init/Kconfig                                  |   8 +-
 kernel/kallsyms.c                             |  46 ++--
 kernel/kallsyms_internal.h                    |   8 +-
 kernel/module/kallsyms.c                      |  85 +++----
 scripts/empty_lineinfo.S                      |  20 +-
 scripts/gen_lineinfo.c                        | 189 +++++++++------
 scripts/kallsyms.c                            |   7 +-
 9 files changed, 406 insertions(+), 191 deletions(-)

diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
index 5cae995eb118e..dd264830c8d5b 100644
--- a/Documentation/admin-guide/kallsyms-lineinfo.rst
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -76,10 +76,11 @@ Memory Overhead
 ===============
 
 The vmlinux lineinfo tables are stored in ``.rodata`` and typically add
-approximately 44 MiB to the kernel image for a standard configuration
-(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication).
+approximately 10-15 MiB to the kernel image for a standard configuration
+(~4.6 million DWARF line entries, ~2-3 bytes per entry after delta
+compression).
 
-Per-module lineinfo adds approximately 10 bytes per DWARF line entry to each
+Per-module lineinfo adds approximately 2-3 bytes per DWARF line entry to each
 ``.ko`` file.
 
 Known Limitations
diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
index d62e9608f0f82..364e5d81fe5bb 100644
--- a/include/linux/mod_lineinfo.h
+++ b/include/linux/mod_lineinfo.h
@@ -8,13 +8,23 @@
  *
  * Section layout (all values in target-native endianness):
  *
- *   struct mod_lineinfo_header     (16 bytes)
- *   u32 addrs[num_entries]         -- offsets from .text base, sorted
- *   u16 file_ids[num_entries]      -- parallel to addrs
- *   <2-byte pad if num_entries is odd>
- *   u32 lines[num_entries]         -- parallel to addrs
+ *   struct mod_lineinfo_header
+ *   u32 block_addrs[num_blocks]    -- first addr per block, for binary search
+ *   u32 block_offsets[num_blocks]  -- byte offset into compressed data stream
+ *   u8  data[data_size]            -- LEB128 delta-compressed entries
  *   u32 file_offsets[num_files]    -- byte offset into filenames[]
  *   char filenames[filenames_size] -- concatenated NUL-terminated strings
+ *
+ * Each sub-array is located by an explicit (offset, size) pair in the
+ * header, similar to a flattened devicetree.  This makes bounds checking
+ * straightforward: validate offset + size <= section_size for each array.
+ *
+ * Compressed stream format (per block of LINEINFO_BLOCK_ENTRIES entries):
+ *   Entry 0: file_id (ULEB128), line (ULEB128)
+ *            addr is in block_addrs[]
+ *   Entry 1..N: addr_delta (ULEB128),
+ *               file_id_delta (SLEB128),
+ *               line_delta (SLEB128)
  */
 #ifndef _LINUX_MOD_LINEINFO_H
 #define _LINUX_MOD_LINEINFO_H
@@ -25,44 +35,209 @@
 #include <stdint.h>
 typedef uint32_t u32;
 typedef uint16_t u16;
+typedef uint8_t  u8;
 #endif
 
+#define LINEINFO_BLOCK_ENTRIES 64
+
 struct mod_lineinfo_header {
 	u32 num_entries;
+	u32 num_blocks;
 	u32 num_files;
-	u32 filenames_size;	/* total bytes of concatenated filenames */
-	u32 reserved;		/* padding, must be 0 */
+	u32 blocks_offset;	/* offset to block_addrs[] from section start */
+	u32 blocks_size;	/* bytes: num_blocks * 2 * sizeof(u32) */
+	u32 data_offset;	/* offset to compressed stream */
+	u32 data_size;		/* bytes of compressed data */
+	u32 files_offset;	/* offset to file_offsets[] */
+	u32 files_size;		/* bytes: num_files * sizeof(u32) */
+	u32 filenames_offset;
+	u32 filenames_size;
+	u32 reserved;		/* must be 0 */
 };
 
-/* Offset helpers: compute byte offset from start of section to each array */
+/*
+ * Descriptor for a lineinfo table, used by the shared lookup function.
+ * Callers populate this from either linker globals (vmlinux) or a
+ * validated mod_lineinfo_header (modules).
+ */
+struct lineinfo_table {
+	const u32 *blk_addrs;
+	const u32 *blk_offsets;
+	const u8  *data;
+	u32 data_size;
+	const u32 *file_offsets;
+	const char *filenames;
+	u32 num_entries;
+	u32 num_blocks;
+	u32 num_files;
+	u32 filenames_size;
+};
 
-static inline u32 mod_lineinfo_addrs_off(void)
+/*
+ * Read a ULEB128 varint from a byte stream.
+ * Returns the decoded value and advances *pos past the encoded bytes.
+ * If *pos would exceed 'end', returns 0 and sets *pos = end (safe for
+ * NMI/panic context: no crash, just a missed annotation).
+ */
+static inline u32 lineinfo_read_uleb128(const u8 *data, u32 *pos, u32 end)
 {
-	return sizeof(struct mod_lineinfo_header);
-}
+	u32 result = 0;
+	unsigned int shift = 0;
 
-static inline u32 mod_lineinfo_file_ids_off(u32 num_entries)
-{
-	return mod_lineinfo_addrs_off() + num_entries * sizeof(u32);
+	while (*pos < end) {
+		u8 byte = data[*pos];
+		(*pos)++;
+		result |= (u32)(byte & 0x7f) << shift;
+		if (!(byte & 0x80))
+			return result;
+		shift += 7;
+		if (shift >= 32) {
+			/* Malformed: skip remaining continuation bytes */
+			while (*pos < end && (data[*pos] & 0x80))
+				(*pos)++;
+			if (*pos < end)
+				(*pos)++;
+			return result;
+		}
+	}
+	return result;
 }
 
-static inline u32 mod_lineinfo_lines_off(u32 num_entries)
+/* Read an SLEB128 varint. Same safety guarantees as above. */
+static inline int32_t lineinfo_read_sleb128(const u8 *data, u32 *pos, u32 end)
 {
-	/* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */
-	u32 off = mod_lineinfo_file_ids_off(num_entries) +
-		  num_entries * sizeof(u16);
-	return (off + 3) & ~3u;
-}
+	int32_t result = 0;
+	unsigned int shift = 0;
+	u8 byte = 0;
 
-static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries)
-{
-	return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32);
+	while (*pos < end) {
+		byte = data[*pos];
+		(*pos)++;
+		result |= (int32_t)(byte & 0x7f) << shift;
+		shift += 7;
+		if (!(byte & 0x80))
+			break;
+		if (shift >= 32) {
+			while (*pos < end && (data[*pos] & 0x80))
+				(*pos)++;
+			if (*pos < end)
+				(*pos)++;
+			return result;
+		}
+	}
+
+	/* Sign-extend if the high bit of the last byte was set */
+	if (shift < 32 && (byte & 0x40))
+		result |= -(1 << shift);
+
+	return result;
 }
 
-static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_files)
+/*
+ * Search a lineinfo table for the source file and line corresponding to a
+ * given offset (from _text for vmlinux, from .text base for modules).
+ *
+ * Safe for NMI and panic context: no locks, no allocations, all state on stack.
+ * Returns true and sets @file and @line on success; false on any failure.
+ */
+static inline bool lineinfo_search(const struct lineinfo_table *tbl,
+				   unsigned int offset,
+				   const char **file, unsigned int *line)
 {
-	return mod_lineinfo_file_offsets_off(num_entries) +
-	       num_files * sizeof(u32);
+	unsigned int low, high, mid, block;
+	unsigned int cur_addr, cur_file_id, cur_line;
+	unsigned int best_file_id = 0, best_line = 0;
+	unsigned int block_entries, data_end;
+	bool found = false;
+	u32 pos;
+
+	if (!tbl->num_entries || !tbl->num_blocks)
+		return false;
+
+	/* Binary search on blk_addrs[] to find the right block */
+	low = 0;
+	high = tbl->num_blocks;
+	while (low < high) {
+		mid = low + (high - low) / 2;
+		if (tbl->blk_addrs[mid] <= offset)
+			low = mid + 1;
+		else
+			high = mid;
+	}
+
+	if (low == 0)
+		return false;
+	block = low - 1;
+
+	/* How many entries in this block? */
+	block_entries = LINEINFO_BLOCK_ENTRIES;
+	if (block == tbl->num_blocks - 1) {
+		unsigned int remaining = tbl->num_entries -
+					block * LINEINFO_BLOCK_ENTRIES;
+
+		if (remaining < block_entries)
+			block_entries = remaining;
+	}
+
+	/* Determine end of this block's data in the compressed stream */
+	if (block + 1 < tbl->num_blocks)
+		data_end = tbl->blk_offsets[block + 1];
+	else
+		data_end = tbl->data_size;
+
+	/* Clamp data_end to actual data size */
+	if (data_end > tbl->data_size)
+		data_end = tbl->data_size;
+
+	/* Decode entry 0: addr from blk_addrs, file_id and line from stream */
+	pos = tbl->blk_offsets[block];
+	if (pos >= data_end)
+		return false;
+
+	cur_addr = tbl->blk_addrs[block];
+	cur_file_id = lineinfo_read_uleb128(tbl->data, &pos, data_end);
+	cur_line = lineinfo_read_uleb128(tbl->data, &pos, data_end);
+
+	/* Check entry 0 */
+	if (cur_addr <= offset) {
+		best_file_id = cur_file_id;
+		best_line = cur_line;
+		found = true;
+	}
+
+	/* Decode entries 1..N */
+	for (unsigned int i = 1; i < block_entries; i++) {
+		unsigned int addr_delta;
+		int32_t file_delta, line_delta;
+
+		addr_delta = lineinfo_read_uleb128(tbl->data, &pos, data_end);
+		file_delta = lineinfo_read_sleb128(tbl->data, &pos, data_end);
+		line_delta = lineinfo_read_sleb128(tbl->data, &pos, data_end);
+
+		cur_addr += addr_delta;
+		cur_file_id = (unsigned int)((int32_t)cur_file_id + file_delta);
+		cur_line = (unsigned int)((int32_t)cur_line + line_delta);
+
+		if (cur_addr > offset)
+			break;
+
+		best_file_id = cur_file_id;
+		best_line = cur_line;
+		found = true;
+	}
+
+	if (!found)
+		return false;
+
+	if (best_file_id >= tbl->num_files)
+		return false;
+
+	if (tbl->file_offsets[best_file_id] >= tbl->filenames_size)
+		return false;
+
+	*file = &tbl->filenames[tbl->file_offsets[best_file_id]];
+	*line = best_line;
+	return true;
 }
 
 #endif /* _LINUX_MOD_LINEINFO_H */
diff --git a/init/Kconfig b/init/Kconfig
index bf53275bc405a..6e3795b3dbd62 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2065,8 +2065,9 @@ config KALLSYMS_LINEINFO
 	    anon_vma_clone+0x2ed/0xcf0 (mm/rmap.c:412)
 
 	  This requires elfutils (libdw-dev/elfutils-devel) on the build host.
-	  Adds approximately 44MB to a typical kernel image (10 bytes per
-	  DWARF line-table entry, ~4.6M entries for a typical config).
+	  Adds approximately 10-15MB to a typical kernel image (~2-3 bytes
+	  per entry after delta compression, ~4.6M entries for a typical
+	  config).
 
 	  If unsure, say N.
 
@@ -2079,7 +2080,8 @@ config KALLSYMS_LINEINFO_MODULES
 	  so stack traces from module code include (file.c:123) annotations.
 
 	  Requires elfutils (libdw-dev/elfutils-devel) on the build host.
-	  Increases .ko sizes by approximately 10 bytes per DWARF line entry.
+	  Increases .ko sizes by approximately 2-3 bytes per DWARF line
+	  entry after delta compression.
 
 	  If unsure, say N.
 
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index 9df92b0fd9041..76e30cac3a277 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -467,13 +467,16 @@ static int append_buildid(char *buffer,   const char *modname,
 
 #endif /* CONFIG_STACKTRACE_BUILD_ID */
 
+#include <linux/mod_lineinfo.h>
+
 bool kallsyms_lookup_lineinfo(unsigned long addr,
 			      const char **file, unsigned int *line)
 {
+	struct lineinfo_table tbl;
 	unsigned long long raw_offset;
-	unsigned int offset, low, high, mid, file_id;
 
-	if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) || !lineinfo_num_entries)
+	if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) ||
+	    !lineinfo_num_entries || !lineinfo_num_blocks)
 		return false;
 
 	/* Compute offset from _text */
@@ -483,34 +486,19 @@ bool kallsyms_lookup_lineinfo(unsigned long addr,
 	raw_offset = addr - (unsigned long)_text;
 	if (raw_offset > UINT_MAX)
 		return false;
-	offset = (unsigned int)raw_offset;
-
-	/* Binary search for largest entry <= offset */
-	low = 0;
-	high = lineinfo_num_entries;
-	while (low < high) {
-		mid = low + (high - low) / 2;
-		if (lineinfo_addrs[mid] <= offset)
-			low = mid + 1;
-		else
-			high = mid;
-	}
-
-	if (low == 0)
-		return false;
-	low--;
-
-	file_id = lineinfo_file_ids[low];
-	*line = lineinfo_lines[low];
-
-	if (file_id >= lineinfo_num_files)
-		return false;
-
-	if (lineinfo_file_offsets[file_id] >= lineinfo_filenames_size)
-		return false;
 
-	*file = &lineinfo_filenames[lineinfo_file_offsets[file_id]];
-	return true;
+	tbl.blk_addrs	= lineinfo_block_addrs;
+	tbl.blk_offsets	= lineinfo_block_offsets;
+	tbl.data	= lineinfo_data;
+	tbl.data_size	= lineinfo_data_size;
+	tbl.file_offsets = lineinfo_file_offsets;
+	tbl.filenames	= lineinfo_filenames;
+	tbl.num_entries	= lineinfo_num_entries;
+	tbl.num_blocks	= lineinfo_num_blocks;
+	tbl.num_files	= lineinfo_num_files;
+	tbl.filenames_size = lineinfo_filenames_size;
+
+	return lineinfo_search(&tbl, (unsigned int)raw_offset, file, line);
 }
 
 /* Look up a kernel symbol and return it in a text buffer. */
diff --git a/kernel/kallsyms_internal.h b/kernel/kallsyms_internal.h
index d7374ce444d81..ffe4c658067ec 100644
--- a/kernel/kallsyms_internal.h
+++ b/kernel/kallsyms_internal.h
@@ -16,10 +16,12 @@ extern const unsigned int kallsyms_markers[];
 extern const u8 kallsyms_seqs_of_names[];
 
 extern const u32 lineinfo_num_entries;
-extern const u32 lineinfo_addrs[];
-extern const u16 lineinfo_file_ids[];
-extern const u32 lineinfo_lines[];
 extern const u32 lineinfo_num_files;
+extern const u32 lineinfo_num_blocks;
+extern const u32 lineinfo_block_addrs[];
+extern const u32 lineinfo_block_offsets[];
+extern const u32 lineinfo_data_size;
+extern const u8  lineinfo_data[];
 extern const u32 lineinfo_file_offsets[];
 extern const u32 lineinfo_filenames_size;
 extern const char lineinfo_filenames[];
diff --git a/kernel/module/kallsyms.c b/kernel/module/kallsyms.c
index 5b46293e957ab..8715a923ba536 100644
--- a/kernel/module/kallsyms.c
+++ b/kernel/module/kallsyms.c
@@ -509,16 +509,11 @@ bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
 			    const char **file, unsigned int *line)
 {
 	const struct mod_lineinfo_header *hdr;
+	struct lineinfo_table tbl;
 	const void *base;
-	const u32 *addrs, *lines, *file_offsets;
-	const u16 *file_ids;
-	const char *filenames;
-	u32 num_entries, num_files, filenames_size;
+	u32 section_size;
 	unsigned long text_base;
-	unsigned int offset;
 	unsigned long long raw_offset;
-	unsigned int low, high, mid;
-	u16 file_id;
 
 	if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES))
 		return false;
@@ -527,61 +522,55 @@ bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
 	if (!base)
 		return false;
 
-	if (mod->lineinfo_data_size < sizeof(*hdr))
+	section_size = mod->lineinfo_data_size;
+	if (section_size < sizeof(*hdr))
 		return false;
 
 	hdr = base;
-	num_entries = hdr->num_entries;
-	num_files = hdr->num_files;
-	filenames_size = hdr->filenames_size;
 
-	if (num_entries == 0)
+	if (hdr->num_entries == 0 || hdr->num_blocks == 0)
 		return false;
 
-	/* Validate section is large enough for all arrays */
-	if (mod->lineinfo_data_size <
-	    mod_lineinfo_filenames_off(num_entries, num_files) + filenames_size)
+	/* Validate each sub-array fits within the section */
+	if (hdr->blocks_offset + hdr->blocks_size > section_size)
 		return false;
-
-	addrs = base + mod_lineinfo_addrs_off();
-	file_ids = base + mod_lineinfo_file_ids_off(num_entries);
-	lines = base + mod_lineinfo_lines_off(num_entries);
-	file_offsets = base + mod_lineinfo_file_offsets_off(num_entries);
-	filenames = base + mod_lineinfo_filenames_off(num_entries, num_files);
-
-	/* Compute offset from module .text base */
-	text_base = (unsigned long)mod->mem[MOD_TEXT].base;
-	if (addr < text_base)
+	if (hdr->data_offset + hdr->data_size > section_size)
 		return false;
-
-	raw_offset = addr - text_base;
-	if (raw_offset > UINT_MAX)
+	if (hdr->files_offset + hdr->files_size > section_size)
+		return false;
+	if (hdr->filenames_offset + hdr->filenames_size > section_size)
 		return false;
-	offset = (unsigned int)raw_offset;
-
-	/* Binary search for largest entry <= offset */
-	low = 0;
-	high = num_entries;
-	while (low < high) {
-		mid = low + (high - low) / 2;
-		if (addrs[mid] <= offset)
-			low = mid + 1;
-		else
-			high = mid;
-	}
 
-	if (low == 0)
+	/* Validate array sizes match declared counts */
+	if (hdr->blocks_size < hdr->num_blocks * 2 * sizeof(u32))
+		return false;
+	if (hdr->files_size < hdr->num_files * sizeof(u32))
 		return false;
-	low--;
 
-	file_id = file_ids[low];
-	if (file_id >= num_files)
+	/*
+	 * Compute offset from module .text base.
+	 * NOTE: This assumes .text is at the start of the MOD_TEXT segment.
+	 * A proper fix would use ELF relocations to reference .text directly.
+	 */
+	text_base = (unsigned long)mod->mem[MOD_TEXT].base;
+	if (addr < text_base)
 		return false;
 
-	if (file_offsets[file_id] >= filenames_size)
+	raw_offset = addr - text_base;
+	if (raw_offset > U32_MAX)
 		return false;
 
-	*file = &filenames[file_offsets[file_id]];
-	*line = lines[low];
-	return true;
+	tbl.blk_addrs	= base + hdr->blocks_offset;
+	tbl.blk_offsets	= base + hdr->blocks_offset +
+			  hdr->num_blocks * sizeof(u32);
+	tbl.data	= base + hdr->data_offset;
+	tbl.data_size	= hdr->data_size;
+	tbl.file_offsets = base + hdr->files_offset;
+	tbl.filenames	= base + hdr->filenames_offset;
+	tbl.num_entries	= hdr->num_entries;
+	tbl.num_blocks	= hdr->num_blocks;
+	tbl.num_files	= hdr->num_files;
+	tbl.filenames_size = hdr->filenames_size;
+
+	return lineinfo_search(&tbl, (unsigned int)raw_offset, file, line);
 }
diff --git a/scripts/empty_lineinfo.S b/scripts/empty_lineinfo.S
index e058c41137123..edd5b1092f050 100644
--- a/scripts/empty_lineinfo.S
+++ b/scripts/empty_lineinfo.S
@@ -14,12 +14,20 @@ lineinfo_num_entries:
 	.balign 4
 lineinfo_num_files:
 	.long 0
-	.globl lineinfo_addrs
-lineinfo_addrs:
-	.globl lineinfo_file_ids
-lineinfo_file_ids:
-	.globl lineinfo_lines
-lineinfo_lines:
+	.globl lineinfo_num_blocks
+	.balign 4
+lineinfo_num_blocks:
+	.long 0
+	.globl lineinfo_block_addrs
+lineinfo_block_addrs:
+	.globl lineinfo_block_offsets
+lineinfo_block_offsets:
+	.globl lineinfo_data_size
+	.balign 4
+lineinfo_data_size:
+	.long 0
+	.globl lineinfo_data
+lineinfo_data:
 	.globl lineinfo_file_offsets
 lineinfo_file_offsets:
 	.globl lineinfo_filenames_size
diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
index 7d06701549345..45b1c1081164d 100644
--- a/scripts/gen_lineinfo.c
+++ b/scripts/gen_lineinfo.c
@@ -548,6 +548,35 @@ static void deduplicate(void)
 	num_entries = j + 1;
 }
 
+/*
+ * Emit the LEB128 delta-compressed data stream for one block.
+ * Uses .uleb128/.sleb128 assembler directives for encoding.
+ */
+static void emit_block_data(unsigned int block)
+{
+	unsigned int base = block * LINEINFO_BLOCK_ENTRIES;
+	unsigned int count = num_entries - base;
+
+	if (count > LINEINFO_BLOCK_ENTRIES)
+		count = LINEINFO_BLOCK_ENTRIES;
+
+	/* Entry 0: file_id, line (both unsigned) */
+	printf("\t.uleb128 %u\n", entries[base].file_id);
+	printf("\t.uleb128 %u\n", entries[base].line);
+
+	/* Entries 1..N: addr_delta (unsigned), file/line deltas (signed) */
+	for (unsigned int i = 1; i < count; i++) {
+		unsigned int idx = base + i;
+
+		printf("\t.uleb128 %u\n",
+		       entries[idx].offset - entries[idx - 1].offset);
+		printf("\t.sleb128 %d\n",
+		       (int)entries[idx].file_id - (int)entries[idx - 1].file_id);
+		printf("\t.sleb128 %d\n",
+		       (int)entries[idx].line - (int)entries[idx - 1].line);
+	}
+}
+
 static void compute_file_offsets(void)
 {
 	unsigned int offset = 0;
@@ -571,6 +600,11 @@ static void print_escaped_asciz(const char *s)
 
 static void output_assembly(void)
 {
+	unsigned int num_blocks;
+
+	num_blocks = num_entries ?
+		(num_entries + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES : 0;
+
 	printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
 	printf("/*\n");
 	printf(" * Automatically generated by scripts/gen_lineinfo\n");
@@ -591,29 +625,40 @@ static void output_assembly(void)
 	printf("lineinfo_num_files:\n");
 	printf("\t.long %u\n\n", num_files);
 
-	/* Sorted address offsets from _text */
-	printf("\t.globl lineinfo_addrs\n");
+	/* Number of blocks */
+	printf("\t.globl lineinfo_num_blocks\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_num_blocks:\n");
+	printf("\t.long %u\n\n", num_blocks);
+
+	/* Block first-addresses for binary search */
+	printf("\t.globl lineinfo_block_addrs\n");
 	printf("\t.balign 4\n");
-	printf("lineinfo_addrs:\n");
-	for (unsigned int i = 0; i < num_entries; i++)
-		printf("\t.long 0x%x\n", entries[i].offset);
-	printf("\n");
-
-	/* File IDs, parallel to addrs (u16 -- supports up to 65535 files) */
-	printf("\t.globl lineinfo_file_ids\n");
-	printf("\t.balign 2\n");
-	printf("lineinfo_file_ids:\n");
-	for (unsigned int i = 0; i < num_entries; i++)
-		printf("\t.short %u\n", entries[i].file_id);
-	printf("\n");
-
-	/* Line numbers, parallel to addrs */
-	printf("\t.globl lineinfo_lines\n");
+	printf("lineinfo_block_addrs:\n");
+	for (unsigned int i = 0; i < num_blocks; i++)
+		printf("\t.long 0x%x\n", entries[i * LINEINFO_BLOCK_ENTRIES].offset);
+
+	/* Block byte offsets into compressed stream */
+	printf("\t.globl lineinfo_block_offsets\n");
 	printf("\t.balign 4\n");
-	printf("lineinfo_lines:\n");
-	for (unsigned int i = 0; i < num_entries; i++)
-		printf("\t.long %u\n", entries[i].line);
-	printf("\n");
+	printf("lineinfo_block_offsets:\n");
+	for (unsigned int i = 0; i < num_blocks; i++)
+		printf("\t.long .Lblock_%u - lineinfo_data\n", i);
+
+	/* Compressed data size */
+	printf("\t.globl lineinfo_data_size\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_data_size:\n");
+	printf("\t.long .Ldata_end - lineinfo_data\n\n");
+
+	/* Compressed data stream */
+	printf("\t.globl lineinfo_data\n");
+	printf("lineinfo_data:\n");
+	for (unsigned int i = 0; i < num_blocks; i++) {
+		printf(".Lblock_%u:\n", i);
+		emit_block_data(i);
+	}
+	printf(".Ldata_end:\n\n");
 
 	/* File string offset table */
 	printf("\t.globl lineinfo_file_offsets\n");
@@ -621,34 +666,27 @@ static void output_assembly(void)
 	printf("lineinfo_file_offsets:\n");
 	for (unsigned int i = 0; i < num_files; i++)
 		printf("\t.long %u\n", files[i].str_offset);
-	printf("\n");
 
 	/* Filenames size */
-	{
-		unsigned int fsize = 0;
-
-		for (unsigned int i = 0; i < num_files; i++)
-			fsize += strlen(files[i].name) + 1;
-		printf("\t.globl lineinfo_filenames_size\n");
-		printf("\t.balign 4\n");
-		printf("lineinfo_filenames_size:\n");
-		printf("\t.long %u\n\n", fsize);
-	}
+	printf("\t.globl lineinfo_filenames_size\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_filenames_size:\n");
+	printf("\t.long .Lfilenames_end - lineinfo_filenames\n\n");
 
 	/* Concatenated NUL-terminated filenames */
 	printf("\t.globl lineinfo_filenames\n");
 	printf("lineinfo_filenames:\n");
 	for (unsigned int i = 0; i < num_files; i++)
 		print_escaped_asciz(files[i].name);
-	printf("\n");
+	printf(".Lfilenames_end:\n");
 }
 
 static void output_module_assembly(void)
 {
-	unsigned int filenames_size = 0;
+	unsigned int num_blocks;
 
-	for (unsigned int i = 0; i < num_files; i++)
-		filenames_size += strlen(files[i].name) + 1;
+	num_blocks = num_entries ?
+		(num_entries + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES : 0;
 
 	printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
 	printf("/*\n");
@@ -658,46 +696,56 @@ static void output_module_assembly(void)
 
 	printf("\t.section .mod_lineinfo, \"a\"\n\n");
 
-	/* Header: num_entries, num_files, filenames_size, reserved */
+	/*
+	 * Header -- offsets and sizes are assembler expressions so the
+	 * layout is self-describing without manual C arithmetic.
+	 */
+	printf(".Lhdr:\n");
 	printf("\t.balign 4\n");
-	printf("\t.long %u\n", num_entries);
-	printf("\t.long %u\n", num_files);
-	printf("\t.long %u\n", filenames_size);
-	printf("\t.long 0\n\n");
-
-	/* addrs[] */
-	for (unsigned int i = 0; i < num_entries; i++)
-		printf("\t.long 0x%x\n", entries[i].offset);
-	if (num_entries)
-		printf("\n");
-
-	/* file_ids[] */
-	for (unsigned int i = 0; i < num_entries; i++)
-		printf("\t.short %u\n", entries[i].file_id);
-
-	/* Padding to align lines[] to 4 bytes */
-	if (num_entries & 1)
-		printf("\t.short 0\n");
-	if (num_entries)
-		printf("\n");
-
-	/* lines[] */
-	for (unsigned int i = 0; i < num_entries; i++)
-		printf("\t.long %u\n", entries[i].line);
-	if (num_entries)
-		printf("\n");
+	printf("\t.long %u\t\t\t\t/* num_entries */\n", num_entries);
+	printf("\t.long %u\t\t\t\t/* num_blocks */\n", num_blocks);
+	printf("\t.long %u\t\t\t\t/* num_files */\n", num_files);
+	printf("\t.long .Lblk_addrs - .Lhdr\t\t/* blocks_offset */\n");
+	printf("\t.long .Lblk_offsets_end - .Lblk_addrs\t/* blocks_size */\n");
+	printf("\t.long .Ldata - .Lhdr\t\t\t/* data_offset */\n");
+	printf("\t.long .Ldata_end - .Ldata\t\t/* data_size */\n");
+	printf("\t.long .Lfile_offsets - .Lhdr\t\t/* files_offset */\n");
+	printf("\t.long .Lfile_offsets_end - .Lfile_offsets /* files_size */\n");
+	printf("\t.long .Lfilenames - .Lhdr\t\t/* filenames_offset */\n");
+	printf("\t.long .Lfilenames_end - .Lfilenames\t/* filenames_size */\n");
+	printf("\t.long 0\t\t\t\t\t/* reserved */\n\n");
+
+	/* block_addrs[] */
+	printf(".Lblk_addrs:\n");
+	for (unsigned int i = 0; i < num_blocks; i++)
+		printf("\t.long 0x%x\n", entries[i * LINEINFO_BLOCK_ENTRIES].offset);
+
+	/* block_offsets[] */
+	printf(".Lblk_offsets:\n");
+	for (unsigned int i = 0; i < num_blocks; i++)
+		printf("\t.long .Lblock_%u - .Ldata\n", i);
+	printf(".Lblk_offsets_end:\n\n");
+
+	/* compressed data stream */
+	printf(".Ldata:\n");
+	for (unsigned int i = 0; i < num_blocks; i++) {
+		printf(".Lblock_%u:\n", i);
+		emit_block_data(i);
+	}
+	printf(".Ldata_end:\n");
 
 	/* file_offsets[] */
+	printf("\t.balign 4\n");
+	printf(".Lfile_offsets:\n");
 	for (unsigned int i = 0; i < num_files; i++)
 		printf("\t.long %u\n", files[i].str_offset);
-	if (num_files)
-		printf("\n");
+	printf(".Lfile_offsets_end:\n\n");
 
 	/* filenames[] */
+	printf(".Lfilenames:\n");
 	for (unsigned int i = 0; i < num_files; i++)
 		print_escaped_asciz(files[i].name);
-	if (num_files)
-		printf("\n");
+	printf(".Lfilenames_end:\n");
 }
 
 int main(int argc, char *argv[])
@@ -777,8 +825,10 @@ int main(int argc, char *argv[])
 	deduplicate();
 	compute_file_offsets();
 
-	fprintf(stderr, "lineinfo: %u entries, %u files\n",
-		num_entries, num_files);
+	fprintf(stderr, "lineinfo: %u entries, %u files, %u blocks\n",
+		num_entries, num_files,
+		num_entries ?
+		(num_entries + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES : 0);
 
 	if (module_mode)
 		output_module_assembly();
@@ -794,6 +844,5 @@ int main(int argc, char *argv[])
 	for (unsigned int i = 0; i < num_files; i++)
 		free(files[i].name);
 	free(files);
-
 	return 0;
 }
diff --git a/scripts/kallsyms.c b/scripts/kallsyms.c
index 42662c4fbc6c9..94fbdad3df7c6 100644
--- a/scripts/kallsyms.c
+++ b/scripts/kallsyms.c
@@ -80,11 +80,12 @@ static bool is_ignored_symbol(const char *name, char type)
 {
 	/* Ignore lineinfo symbols for kallsyms pass stability */
 	static const char * const lineinfo_syms[] = {
-		"lineinfo_addrs",
-		"lineinfo_file_ids",
+		"lineinfo_block_addrs",
+		"lineinfo_block_offsets",
+		"lineinfo_data",
 		"lineinfo_file_offsets",
 		"lineinfo_filenames",
-		"lineinfo_lines",
+		"lineinfo_num_blocks",
 		"lineinfo_num_entries",
 		"lineinfo_num_files",
 	};
-- 
2.51.0


^ permalink raw reply related

* [PATCH v4 2/4] kallsyms: extend lineinfo to loadable modules
From: Sasha Levin @ 2026-03-22 13:15 UTC (permalink / raw)
  To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, Helge Deller,
	Randy Dunlap, Laurent Pinchart, Vivian Wang, linux-kernel,
	linux-kbuild, linux-modules, linux-doc, Sasha Levin
In-Reply-To: <20260322131543.971079-1-sashal@kernel.org>

Add CONFIG_KALLSYMS_LINEINFO_MODULES, which extends the
CONFIG_KALLSYMS_LINEINFO feature to loadable kernel modules.

At build time, each .ko is post-processed by scripts/gen-mod-lineinfo.sh
(modeled on gen-btf.sh) which runs scripts/gen_lineinfo --module on the
.ko, generates a .mod_lineinfo section containing a compact binary table
of .text-relative offsets, file IDs, line numbers, and filenames, and
embeds it back into the .ko via objcopy.

At runtime, module_lookup_lineinfo() performs a binary search on the
module's .mod_lineinfo section, and __sprint_symbol() calls it for
addresses that fall within a module.  The lookup is NMI/panic-safe
(no locks, no allocations) — the data lives in read-only module memory
and is freed automatically when the module is unloaded.

The gen_lineinfo tool gains --module mode which:
 - Uses .text section address as base (ET_REL files have no _text symbol)
 - Filters entries to .text-only (excludes .init.text/.exit.text)
 - Handles libdw's ET_REL path-doubling quirk in make_relative()
 - Outputs a flat binary-format section instead of named global symbols

Per-module overhead is approximately 10 bytes per DWARF line entry.

Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
 .../admin-guide/kallsyms-lineinfo.rst         |  40 +-
 MAINTAINERS                                   |   2 +
 include/linux/mod_lineinfo.h                  |  68 ++++
 include/linux/module.h                        |   5 +
 init/Kconfig                                  |  13 +
 kernel/kallsyms.c                             |  18 +-
 kernel/module/kallsyms.c                      |  91 +++++
 kernel/module/main.c                          |   3 +
 scripts/Makefile.modfinal                     |   6 +
 scripts/gen-mod-lineinfo.sh                   |  48 +++
 scripts/gen_lineinfo.c                        | 347 ++++++++++++++++--
 11 files changed, 601 insertions(+), 40 deletions(-)
 create mode 100644 include/linux/mod_lineinfo.h
 create mode 100755 scripts/gen-mod-lineinfo.sh

diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
index c8ec124394354..5cae995eb118e 100644
--- a/Documentation/admin-guide/kallsyms-lineinfo.rst
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -51,22 +51,46 @@ With ``CONFIG_KALLSYMS_LINEINFO``::
 Note that assembly routines (such as ``entry_SYSCALL_64_after_hwframe``) are
 not annotated because they lack DWARF debug information.
 
+Module Support
+==============
+
+``CONFIG_KALLSYMS_LINEINFO_MODULES`` extends the feature to loadable kernel
+modules.  When enabled, each ``.ko`` is post-processed at build time to embed
+a ``.mod_lineinfo`` section containing the same kind of address-to-source
+mapping.
+
+Enable in addition to the base options::
+
+    CONFIG_MODULES=y
+    CONFIG_KALLSYMS_LINEINFO_MODULES=y
+
+Stack traces from module code will then include annotations::
+
+    my_driver_func+0x30/0x100 [my_driver] (drivers/foo/bar.c:123)
+
+The ``.mod_lineinfo`` section is loaded into read-only module memory alongside
+the module text.  No additional runtime memory allocation is required; the data
+is freed when the module is unloaded.
+
 Memory Overhead
 ===============
 
-The lineinfo tables are stored in ``.rodata`` and typically add approximately
-44 MiB to the kernel image for a standard configuration (~4.6 million DWARF
-line entries, ~10 bytes per entry after deduplication).
+The vmlinux lineinfo tables are stored in ``.rodata`` and typically add
+approximately 44 MiB to the kernel image for a standard configuration
+(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication).
+
+Per-module lineinfo adds approximately 10 bytes per DWARF line entry to each
+``.ko`` file.
 
 Known Limitations
 =================
 
-- **vmlinux only**: Only symbols in the core kernel image are annotated.
-  Module symbols are not covered.
-- **4 GiB offset limit**: Address offsets from ``_text`` are stored as 32-bit
-  values.  Entries beyond 4 GiB from ``_text`` are skipped at build time with
-  a warning.
+- **4 GiB offset limit**: Address offsets from ``_text`` (vmlinux) or
+  ``.text`` base (modules) are stored as 32-bit values.  Entries beyond
+  4 GiB are skipped at build time with a warning.
 - **65535 file limit**: Source file IDs are stored as 16-bit values.  Builds
   with more than 65535 unique source files will fail with an error.
 - **No assembly annotations**: Functions implemented in assembly that lack
   DWARF ``.debug_line`` data are not annotated.
+- **No init text**: For modules, functions in ``.init.text`` are not annotated
+  because that memory is freed after module initialization.
diff --git a/MAINTAINERS b/MAINTAINERS
index f061e69b6e32a..535e992ca5a20 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13732,6 +13732,8 @@ KALLSYMS LINEINFO
 M:	Sasha Levin <sashal@kernel.org>
 S:	Maintained
 F:	Documentation/admin-guide/kallsyms-lineinfo.rst
+F:	include/linux/mod_lineinfo.h
+F:	scripts/gen-mod-lineinfo.sh
 F:	scripts/gen_lineinfo.c
 
 KASAN
diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
new file mode 100644
index 0000000000000..d62e9608f0f82
--- /dev/null
+++ b/include/linux/mod_lineinfo.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * mod_lineinfo.h - Binary format for per-module source line information
+ *
+ * This header defines the layout of the .mod_lineinfo section embedded
+ * in loadable kernel modules.  It is dual-use: included from both the
+ * kernel and the userspace gen_lineinfo tool.
+ *
+ * Section layout (all values in target-native endianness):
+ *
+ *   struct mod_lineinfo_header     (16 bytes)
+ *   u32 addrs[num_entries]         -- offsets from .text base, sorted
+ *   u16 file_ids[num_entries]      -- parallel to addrs
+ *   <2-byte pad if num_entries is odd>
+ *   u32 lines[num_entries]         -- parallel to addrs
+ *   u32 file_offsets[num_files]    -- byte offset into filenames[]
+ *   char filenames[filenames_size] -- concatenated NUL-terminated strings
+ */
+#ifndef _LINUX_MOD_LINEINFO_H
+#define _LINUX_MOD_LINEINFO_H
+
+#ifdef __KERNEL__
+#include <linux/types.h>
+#else
+#include <stdint.h>
+typedef uint32_t u32;
+typedef uint16_t u16;
+#endif
+
+struct mod_lineinfo_header {
+	u32 num_entries;
+	u32 num_files;
+	u32 filenames_size;	/* total bytes of concatenated filenames */
+	u32 reserved;		/* padding, must be 0 */
+};
+
+/* Offset helpers: compute byte offset from start of section to each array */
+
+static inline u32 mod_lineinfo_addrs_off(void)
+{
+	return sizeof(struct mod_lineinfo_header);
+}
+
+static inline u32 mod_lineinfo_file_ids_off(u32 num_entries)
+{
+	return mod_lineinfo_addrs_off() + num_entries * sizeof(u32);
+}
+
+static inline u32 mod_lineinfo_lines_off(u32 num_entries)
+{
+	/* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */
+	u32 off = mod_lineinfo_file_ids_off(num_entries) +
+		  num_entries * sizeof(u16);
+	return (off + 3) & ~3u;
+}
+
+static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries)
+{
+	return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32);
+}
+
+static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_files)
+{
+	return mod_lineinfo_file_offsets_off(num_entries) +
+	       num_files * sizeof(u32);
+}
+
+#endif /* _LINUX_MOD_LINEINFO_H */
diff --git a/include/linux/module.h b/include/linux/module.h
index 14f391b186c6d..d23e0cd9c7210 100644
--- a/include/linux/module.h
+++ b/include/linux/module.h
@@ -508,6 +508,8 @@ struct module {
 	void *btf_data;
 	void *btf_base_data;
 #endif
+	void *lineinfo_data;		/* .mod_lineinfo section in MOD_RODATA */
+	unsigned int lineinfo_data_size;
 #ifdef CONFIG_JUMP_LABEL
 	struct jump_entry *jump_entries;
 	unsigned int num_jump_entries;
@@ -1021,6 +1023,9 @@ static inline unsigned long find_kallsyms_symbol_value(struct module *mod,
 
 #endif  /* CONFIG_MODULES && CONFIG_KALLSYMS */
 
+bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
+			    const char **file, unsigned int *line);
+
 /* Define __free(module_put) macro for struct module *. */
 DEFINE_FREE(module_put, struct module *, if (_T) module_put(_T))
 
diff --git a/init/Kconfig b/init/Kconfig
index c39f27e6393a8..bf53275bc405a 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2070,6 +2070,19 @@ config KALLSYMS_LINEINFO
 
 	  If unsure, say N.
 
+config KALLSYMS_LINEINFO_MODULES
+	bool "Embed source file:line information in module stack traces"
+	depends on KALLSYMS_LINEINFO && MODULES
+	help
+	  Extends KALLSYMS_LINEINFO to loadable kernel modules.  Each .ko
+	  gets a lineinfo table generated from its DWARF data at build time,
+	  so stack traces from module code include (file.c:123) annotations.
+
+	  Requires elfutils (libdw-dev/elfutils-devel) on the build host.
+	  Increases .ko sizes by approximately 10 bytes per DWARF line entry.
+
+	  If unsure, say N.
+
 # end of the "standard kernel features (expert users)" menu
 
 config ARCH_HAS_MEMBARRIER_CALLBACKS
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index d0a9cd9c6dace..9df92b0fd9041 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -543,12 +543,24 @@ static int __sprint_symbol(char *buffer, unsigned long address,
 		len += sprintf(buffer + len, "]");
 	}
 
-	if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) && !modname) {
+	if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO)) {
 		const char *li_file;
 		unsigned int li_line;
+		bool found = false;
+
+		if (!modname)
+			found = kallsyms_lookup_lineinfo(address,
+							 &li_file, &li_line);
+		else if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES)) {
+			struct module *mod = __module_address(address);
+
+			if (mod)
+				found = module_lookup_lineinfo(mod, address,
+							      &li_file,
+							      &li_line);
+		}
 
-		if (kallsyms_lookup_lineinfo(address,
-					     &li_file, &li_line))
+		if (found)
 			len += snprintf(buffer + len, KSYM_SYMBOL_LEN - len,
 					" (%s:%u)", li_file, li_line);
 	}
diff --git a/kernel/module/kallsyms.c b/kernel/module/kallsyms.c
index 0fc11e45df9b9..5b46293e957ab 100644
--- a/kernel/module/kallsyms.c
+++ b/kernel/module/kallsyms.c
@@ -494,3 +494,94 @@ int module_kallsyms_on_each_symbol(const char *modname,
 	mutex_unlock(&module_mutex);
 	return ret;
 }
+
+#include <linux/mod_lineinfo.h>
+
+/*
+ * Look up source file:line for an address within a loaded module.
+ * Uses the .mod_lineinfo section embedded in the .ko at build time.
+ *
+ * Safe in NMI/panic context: no locks, no allocations.
+ * Caller must hold RCU read lock (or be in a context where the module
+ * cannot be unloaded).
+ */
+bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
+			    const char **file, unsigned int *line)
+{
+	const struct mod_lineinfo_header *hdr;
+	const void *base;
+	const u32 *addrs, *lines, *file_offsets;
+	const u16 *file_ids;
+	const char *filenames;
+	u32 num_entries, num_files, filenames_size;
+	unsigned long text_base;
+	unsigned int offset;
+	unsigned long long raw_offset;
+	unsigned int low, high, mid;
+	u16 file_id;
+
+	if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES))
+		return false;
+
+	base = mod->lineinfo_data;
+	if (!base)
+		return false;
+
+	if (mod->lineinfo_data_size < sizeof(*hdr))
+		return false;
+
+	hdr = base;
+	num_entries = hdr->num_entries;
+	num_files = hdr->num_files;
+	filenames_size = hdr->filenames_size;
+
+	if (num_entries == 0)
+		return false;
+
+	/* Validate section is large enough for all arrays */
+	if (mod->lineinfo_data_size <
+	    mod_lineinfo_filenames_off(num_entries, num_files) + filenames_size)
+		return false;
+
+	addrs = base + mod_lineinfo_addrs_off();
+	file_ids = base + mod_lineinfo_file_ids_off(num_entries);
+	lines = base + mod_lineinfo_lines_off(num_entries);
+	file_offsets = base + mod_lineinfo_file_offsets_off(num_entries);
+	filenames = base + mod_lineinfo_filenames_off(num_entries, num_files);
+
+	/* Compute offset from module .text base */
+	text_base = (unsigned long)mod->mem[MOD_TEXT].base;
+	if (addr < text_base)
+		return false;
+
+	raw_offset = addr - text_base;
+	if (raw_offset > UINT_MAX)
+		return false;
+	offset = (unsigned int)raw_offset;
+
+	/* Binary search for largest entry <= offset */
+	low = 0;
+	high = num_entries;
+	while (low < high) {
+		mid = low + (high - low) / 2;
+		if (addrs[mid] <= offset)
+			low = mid + 1;
+		else
+			high = mid;
+	}
+
+	if (low == 0)
+		return false;
+	low--;
+
+	file_id = file_ids[low];
+	if (file_id >= num_files)
+		return false;
+
+	if (file_offsets[file_id] >= filenames_size)
+		return false;
+
+	*file = &filenames[file_offsets[file_id]];
+	*line = lines[low];
+	return true;
+}
diff --git a/kernel/module/main.c b/kernel/module/main.c
index 2bac4c7cd019a..d11646b02730a 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2648,6 +2648,9 @@ static int find_module_sections(struct module *mod, struct load_info *info)
 	mod->btf_base_data = any_section_objs(info, ".BTF.base", 1,
 					      &mod->btf_base_data_size);
 #endif
+	if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES))
+		mod->lineinfo_data = any_section_objs(info, ".mod_lineinfo", 1,
+						      &mod->lineinfo_data_size);
 #ifdef CONFIG_JUMP_LABEL
 	mod->jump_entries = section_objs(info, "__jump_table",
 					sizeof(*mod->jump_entries),
diff --git a/scripts/Makefile.modfinal b/scripts/Makefile.modfinal
index adcbcde16a071..3941cf624526b 100644
--- a/scripts/Makefile.modfinal
+++ b/scripts/Makefile.modfinal
@@ -46,6 +46,9 @@ quiet_cmd_btf_ko = BTF [M] $@
 		$(CONFIG_SHELL) $(srctree)/scripts/gen-btf.sh --btf_base $(objtree)/vmlinux $@; \
 	fi;
 
+quiet_cmd_lineinfo_ko = LINEINFO [M] $@
+      cmd_lineinfo_ko = $(CONFIG_SHELL) $(srctree)/scripts/gen-mod-lineinfo.sh $@
+
 # Same as newer-prereqs, but allows to exclude specified extra dependencies
 newer_prereqs_except = $(filter-out $(PHONY) $(1),$?)
 
@@ -59,6 +62,9 @@ if_changed_except = $(if $(call newer_prereqs_except,$(2))$(cmd-check),      \
 	+$(call if_changed_except,ld_ko_o,$(objtree)/vmlinux)
 ifdef CONFIG_DEBUG_INFO_BTF_MODULES
 	+$(if $(newer-prereqs),$(call cmd,btf_ko))
+endif
+ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+	+$(if $(newer-prereqs),$(call cmd,lineinfo_ko))
 endif
 	+$(call cmd,check_tracepoint)
 
diff --git a/scripts/gen-mod-lineinfo.sh b/scripts/gen-mod-lineinfo.sh
new file mode 100755
index 0000000000000..d0663b862d31b
--- /dev/null
+++ b/scripts/gen-mod-lineinfo.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+#
+# gen-mod-lineinfo.sh - Embed source line info into a kernel module (.ko)
+#
+# Reads DWARF from the .ko, generates a .mod_lineinfo section, and
+# embeds it back into the .ko.  Modeled on scripts/gen-btf.sh.
+
+set -e
+
+if [ $# -ne 1 ]; then
+	echo "Usage: $0 <module.ko>" >&2
+	exit 1
+fi
+
+KO="$1"
+
+cleanup() {
+	rm -f "${KO}.lineinfo.S" "${KO}.lineinfo.o" "${KO}.lineinfo.bin"
+}
+trap cleanup EXIT
+
+case "${KBUILD_VERBOSE}" in
+*1*)
+	set -x
+	;;
+esac
+
+# Generate assembly from DWARF -- if it fails (no DWARF), silently skip
+if ! ${objtree}/scripts/gen_lineinfo --module "${KO}" > "${KO}.lineinfo.S"; then
+	exit 0
+fi
+
+# Compile assembly to object file
+${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+	${KBUILD_AFLAGS} ${KBUILD_AFLAGS_MODULE} \
+	-c -o "${KO}.lineinfo.o" "${KO}.lineinfo.S"
+
+# Extract raw section content
+${OBJCOPY} -O binary --only-section=.mod_lineinfo \
+	"${KO}.lineinfo.o" "${KO}.lineinfo.bin"
+
+# Embed into the .ko with alloc,readonly flags
+${OBJCOPY} --add-section ".mod_lineinfo=${KO}.lineinfo.bin" \
+	--set-section-flags .mod_lineinfo=alloc,readonly \
+	"${KO}"
+
+exit 0
diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
index 37d5e84971be4..7d06701549345 100644
--- a/scripts/gen_lineinfo.c
+++ b/scripts/gen_lineinfo.c
@@ -23,8 +23,16 @@
 #include <gelf.h>
 #include <limits.h>
 
+#include "../include/linux/mod_lineinfo.h"
+
+static int module_mode;
+
 static unsigned int skipped_overflow;
 
+/* .text range for module mode (keep only runtime code) */
+static unsigned long long text_section_start;
+static unsigned long long text_section_end;
+
 struct line_entry {
 	unsigned int offset;	/* offset from _text */
 	unsigned int file_id;
@@ -148,27 +156,25 @@ static const char *make_relative(const char *path, const char *comp_dir)
 {
 	const char *p;
 
-	/* If already relative, use as-is */
-	if (path[0] != '/')
-		return path;
-
-	/* comp_dir from DWARF is the most reliable method */
-	if (comp_dir) {
-		size_t len = strlen(comp_dir);
-
-		if (!strncmp(path, comp_dir, len) && path[len] == '/') {
-			const char *rel = path + len + 1;
-
-			/*
-			 * If comp_dir pointed to a subdirectory
-			 * (e.g. arch/parisc/kernel) rather than
-			 * the tree root, stripping it leaves a
-			 * bare filename.  Fall through to the
-			 * kernel_dirs scan so we recover the full
-			 * relative path instead.
-			 */
-			if (strchr(rel, '/'))
-				return rel;
+	if (path[0] == '/') {
+		/* Try comp_dir prefix from DWARF */
+		if (comp_dir) {
+			size_t len = strlen(comp_dir);
+
+			if (!strncmp(path, comp_dir, len) && path[len] == '/') {
+				const char *rel = path + len + 1;
+
+				/*
+				 * If comp_dir pointed to a subdirectory
+				 * (e.g. arch/parisc/kernel) rather than
+				 * the tree root, stripping it leaves a
+				 * bare filename.  Fall through to the
+				 * kernel_dirs scan so we recover the full
+				 * relative path instead.
+				 */
+				if (strchr(rel, '/'))
+					return rel;
+			}
 		}
 
 		/*
@@ -194,9 +200,42 @@ static const char *make_relative(const char *path, const char *comp_dir)
 		return p ? p + 1 : path;
 	}
 
-	/* Fall back to basename */
-	p = strrchr(path, '/');
-	return p ? p + 1 : path;
+	/*
+	 * Relative path — check for duplicated-path quirk from libdw
+	 * on ET_REL files (e.g., "a/b.c/a/b.c" → "a/b.c").
+	 */
+	{
+		size_t len = strlen(path);
+		size_t mid = len / 2;
+
+		if (len > 1 && path[mid] == '/' &&
+		    !memcmp(path, path + mid + 1, mid))
+			return path + mid + 1;
+	}
+
+	/*
+	 * Bare filename with no directory component — try to recover the
+	 * relative path using comp_dir.  Some toolchains/elfutils combos
+	 * produce bare filenames where comp_dir holds the source directory.
+	 * Construct the absolute path and run the kernel_dirs scan.
+	 */
+	if (!strchr(path, '/') && comp_dir && comp_dir[0] == '/') {
+		static char buf[PATH_MAX];
+
+		snprintf(buf, sizeof(buf), "%s/%s", comp_dir, path);
+		for (p = buf + 1; *p; p++) {
+			if (*(p - 1) == '/') {
+				for (unsigned int i = 0; i < sizeof(kernel_dirs) /
+				     sizeof(kernel_dirs[0]); i++) {
+					if (!strncmp(p, kernel_dirs[i],
+						     strlen(kernel_dirs[i])))
+						return p;
+				}
+			}
+		}
+	}
+
+	return path;
 }
 
 static int compare_entries(const void *a, const void *b)
@@ -248,6 +287,159 @@ static unsigned long long find_text_addr(Elf *elf)
 	exit(1);
 }
 
+static void find_text_section_range(Elf *elf)
+{
+	Elf_Scn *scn = NULL;
+	GElf_Shdr shdr;
+	size_t shstrndx;
+
+	if (elf_getshdrstrndx(elf, &shstrndx) != 0)
+		return;
+
+	while ((scn = elf_nextscn(elf, scn)) != NULL) {
+		const char *name;
+
+		if (!gelf_getshdr(scn, &shdr))
+			continue;
+		name = elf_strptr(elf, shstrndx, shdr.sh_name);
+		if (name && !strcmp(name, ".text")) {
+			text_section_start = shdr.sh_addr;
+			text_section_end = shdr.sh_addr + shdr.sh_size;
+			return;
+		}
+	}
+}
+
+/*
+ * Apply .rela.debug_line relocations to a mutable copy of .debug_line data.
+ *
+ * elfutils libdw (through at least 0.194) does NOT apply relocations for
+ * ET_REL files when using dwarf_begin_elf().  The internal libdwfl layer
+ * does this via __libdwfl_relocate(), but that API is not public.
+ *
+ * For DWARF5, the .debug_line file name table uses DW_FORM_line_strp
+ * references into .debug_line_str.  Without relocation, all these offsets
+ * resolve to 0 (or garbage), causing dwarf_linesrc()/dwarf_filesrc() to
+ * return wrong filenames (typically the comp_dir for every file).
+ *
+ * This function applies the relocations manually so that the patched
+ * .debug_line data can be fed to dwarf_begin_elf() and produce correct
+ * results.
+ *
+ * See elfutils bug https://sourceware.org/bugzilla/show_bug.cgi?id=31447
+ * A fix (dwelf_elf_apply_relocs) was proposed but not yet merged as of
+ * elfutils 0.194: https://sourceware.org/pipermail/elfutils-devel/2024q3/007388.html
+ */
+/*
+ * Determine the relocation type for a 32-bit absolute reference
+ * on the given architecture.  Returns 0 if unknown.
+ */
+static unsigned int r_type_abs32(unsigned int e_machine)
+{
+	switch (e_machine) {
+	case EM_X86_64:		return R_X86_64_32;
+	case EM_386:		return R_386_32;
+	case EM_AARCH64:	return R_AARCH64_ABS32;
+	case EM_ARM:		return R_ARM_ABS32;
+	case EM_RISCV:		return R_RISCV_32;
+	case EM_S390:		return R_390_32;
+	case EM_MIPS:		return R_MIPS_32;
+	case EM_PPC64:		return R_PPC64_ADDR32;
+	case EM_PPC:		return R_PPC_ADDR32;
+	case EM_LOONGARCH:	return R_LARCH_32;
+	case EM_PARISC:		return R_PARISC_DIR32;
+	default:		return 0;
+	}
+}
+
+static void apply_debug_line_relocations(Elf *elf)
+{
+	Elf_Scn *scn = NULL;
+	Elf_Scn *debug_line_scn = NULL;
+	Elf_Scn *rela_debug_line_scn = NULL;
+	Elf_Scn *symtab_scn = NULL;
+	GElf_Shdr shdr;
+	GElf_Ehdr ehdr;
+	unsigned int abs32_type;
+	size_t shstrndx;
+	Elf_Data *dl_data, *rela_data, *sym_data;
+	GElf_Shdr rela_shdr, sym_shdr;
+	size_t nrels, i;
+
+	if (gelf_getehdr(elf, &ehdr) == NULL)
+		return;
+
+	abs32_type = r_type_abs32(ehdr.e_machine);
+	if (!abs32_type)
+		return;
+
+	if (elf_getshdrstrndx(elf, &shstrndx) != 0)
+		return;
+
+	/* Find the relevant sections */
+	while ((scn = elf_nextscn(elf, scn)) != NULL) {
+		const char *name;
+
+		if (!gelf_getshdr(scn, &shdr))
+			continue;
+		name = elf_strptr(elf, shstrndx, shdr.sh_name);
+		if (!name)
+			continue;
+
+		if (!strcmp(name, ".debug_line"))
+			debug_line_scn = scn;
+		else if (!strcmp(name, ".rela.debug_line"))
+			rela_debug_line_scn = scn;
+		else if (shdr.sh_type == SHT_SYMTAB)
+			symtab_scn = scn;
+	}
+
+	if (!debug_line_scn || !rela_debug_line_scn || !symtab_scn)
+		return;
+
+	dl_data = elf_getdata(debug_line_scn, NULL);
+	rela_data = elf_getdata(rela_debug_line_scn, NULL);
+	sym_data = elf_getdata(symtab_scn, NULL);
+	if (!dl_data || !rela_data || !sym_data)
+		return;
+
+	if (!gelf_getshdr(rela_debug_line_scn, &rela_shdr))
+		return;
+	if (!gelf_getshdr(symtab_scn, &sym_shdr))
+		return;
+
+	nrels = rela_shdr.sh_size / rela_shdr.sh_entsize;
+
+	for (i = 0; i < nrels; i++) {
+		GElf_Rela rela;
+		GElf_Sym sym;
+		unsigned int r_type;
+		size_t r_sym;
+		uint32_t value;
+
+		if (!gelf_getrela(rela_data, i, &rela))
+			continue;
+
+		r_type = GELF_R_TYPE(rela.r_info);
+		r_sym = GELF_R_SYM(rela.r_info);
+
+		/* Only handle the 32-bit absolute reloc for this arch */
+		if (r_type != abs32_type)
+			continue;
+
+		if (!gelf_getsym(sym_data, r_sym, &sym))
+			continue;
+
+		/* Relocated value = sym.st_value + addend */
+		value = (uint32_t)(sym.st_value + rela.r_addend);
+
+		/* Patch the .debug_line data at the relocation offset */
+		if (rela.r_offset + 4 <= dl_data->d_size)
+			memcpy((char *)dl_data->d_buf + rela.r_offset,
+			       &value, sizeof(value));
+	}
+}
+
 static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
 {
 	Dwarf_Off off = 0, next_off;
@@ -295,6 +487,17 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
 			if (addr < text_addr)
 				continue;
 
+			/*
+			 * In module mode, keep only .text addresses.
+			 * In ET_REL .ko files, .text, .init.text and
+			 * .exit.text all have sh_addr == 0 and therefore
+			 * overlapping address ranges.  Explicitly check
+			 * against the .text bounds.
+			 */
+			if (module_mode && text_section_end > text_section_start &&
+			    (addr < text_section_start || addr >= text_section_end))
+				continue;
+
 			{
 				unsigned long long raw_offset = addr - text_addr;
 
@@ -440,6 +643,63 @@ static void output_assembly(void)
 	printf("\n");
 }
 
+static void output_module_assembly(void)
+{
+	unsigned int filenames_size = 0;
+
+	for (unsigned int i = 0; i < num_files; i++)
+		filenames_size += strlen(files[i].name) + 1;
+
+	printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
+	printf("/*\n");
+	printf(" * Automatically generated by scripts/gen_lineinfo --module\n");
+	printf(" * Do not edit.\n");
+	printf(" */\n\n");
+
+	printf("\t.section .mod_lineinfo, \"a\"\n\n");
+
+	/* Header: num_entries, num_files, filenames_size, reserved */
+	printf("\t.balign 4\n");
+	printf("\t.long %u\n", num_entries);
+	printf("\t.long %u\n", num_files);
+	printf("\t.long %u\n", filenames_size);
+	printf("\t.long 0\n\n");
+
+	/* addrs[] */
+	for (unsigned int i = 0; i < num_entries; i++)
+		printf("\t.long 0x%x\n", entries[i].offset);
+	if (num_entries)
+		printf("\n");
+
+	/* file_ids[] */
+	for (unsigned int i = 0; i < num_entries; i++)
+		printf("\t.short %u\n", entries[i].file_id);
+
+	/* Padding to align lines[] to 4 bytes */
+	if (num_entries & 1)
+		printf("\t.short 0\n");
+	if (num_entries)
+		printf("\n");
+
+	/* lines[] */
+	for (unsigned int i = 0; i < num_entries; i++)
+		printf("\t.long %u\n", entries[i].line);
+	if (num_entries)
+		printf("\n");
+
+	/* file_offsets[] */
+	for (unsigned int i = 0; i < num_files; i++)
+		printf("\t.long %u\n", files[i].str_offset);
+	if (num_files)
+		printf("\n");
+
+	/* filenames[] */
+	for (unsigned int i = 0; i < num_files; i++)
+		print_escaped_asciz(files[i].name);
+	if (num_files)
+		printf("\n");
+}
+
 int main(int argc, char *argv[])
 {
 	int fd;
@@ -447,12 +707,23 @@ int main(int argc, char *argv[])
 	Dwarf *dwarf;
 	unsigned long long text_addr;
 
+	if (argc >= 2 && !strcmp(argv[1], "--module")) {
+		module_mode = 1;
+		argv++;
+		argc--;
+	}
+
 	if (argc != 2) {
-		fprintf(stderr, "Usage: %s <vmlinux>\n", argv[0]);
+		fprintf(stderr, "Usage: %s [--module] <ELF file>\n", argv[0]);
 		return 1;
 	}
 
-	fd = open(argv[1], O_RDONLY);
+	/*
+	 * For module mode, open O_RDWR so we can apply debug section
+	 * relocations to the in-memory ELF data.  The modifications
+	 * are NOT written back to disk (no elf_update() call).
+	 */
+	fd = open(argv[1], module_mode ? O_RDWR : O_RDONLY);
 	if (fd < 0) {
 		fprintf(stderr, "Cannot open %s: %s\n", argv[1],
 			strerror(errno));
@@ -460,7 +731,7 @@ int main(int argc, char *argv[])
 	}
 
 	elf_version(EV_CURRENT);
-	elf = elf_begin(fd, ELF_C_READ, NULL);
+	elf = elf_begin(fd, module_mode ? ELF_C_RDWR : ELF_C_READ, NULL);
 	if (!elf) {
 		fprintf(stderr, "elf_begin failed: %s\n",
 			elf_errmsg(elf_errno()));
@@ -468,7 +739,22 @@ int main(int argc, char *argv[])
 		return 1;
 	}
 
-	text_addr = find_text_addr(elf);
+	if (module_mode) {
+		/*
+		 * .ko files are ET_REL after ld -r.  libdw does NOT apply
+		 * relocations for ET_REL files, so DW_FORM_line_strp
+		 * references in .debug_line are not resolved.  Apply them
+		 * ourselves so that dwarf_linesrc() returns correct paths.
+		 *
+		 * DWARF addresses include the .text sh_addr.  Use .text
+		 * sh_addr as the base so offsets are .text-relative.
+		 */
+		apply_debug_line_relocations(elf);
+		find_text_section_range(elf);
+		text_addr = text_section_start;
+	} else {
+		text_addr = find_text_addr(elf);
+	}
 
 	dwarf = dwarf_begin_elf(elf, DWARF_C_READ, NULL);
 	if (!dwarf) {
@@ -494,7 +780,10 @@ int main(int argc, char *argv[])
 	fprintf(stderr, "lineinfo: %u entries, %u files\n",
 		num_entries, num_files);
 
-	output_assembly();
+	if (module_mode)
+		output_module_assembly();
+	else
+		output_assembly();
 
 	dwarf_end(dwarf);
 	elf_end(elf);
-- 
2.51.0


^ permalink raw reply related

* [PATCH v4 1/4] kallsyms: embed source file:line info in kernel stack traces
From: Sasha Levin @ 2026-03-22 13:15 UTC (permalink / raw)
  To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, Helge Deller,
	Randy Dunlap, Laurent Pinchart, Vivian Wang, linux-kernel,
	linux-kbuild, linux-modules, linux-doc, Sasha Levin
In-Reply-To: <20260322131543.971079-1-sashal@kernel.org>

Add CONFIG_KALLSYMS_LINEINFO, which embeds a compact address-to-line
lookup table in the kernel image so stack traces directly print source
file and line number information:

  root@localhost:~# echo c > /proc/sysrq-trigger
  [   11.201987] sysrq: Trigger a crash
  [   11.202831] Kernel panic - not syncing: sysrq triggered crash
  [   11.206218] Call Trace:
  [   11.206501]  <TASK>
  [   11.206749]  dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
  [   11.207403]  vpanic+0x36e/0x620 (kernel/panic.c:650)
  [   11.208565]  ? __lock_acquire+0x465/0x2240 (kernel/locking/lockdep.c:4674)
  [   11.209324]  panic+0xc9/0xd0 (kernel/panic.c:787)
  [   11.211873]  ? find_held_lock+0x2b/0x80 (kernel/locking/lockdep.c:5350)
  [   11.212597]  ? lock_release+0xd3/0x300 (kernel/locking/lockdep.c:5535)
  [   11.213312]  sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
  [   11.214005]  __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
  [   11.214712]  write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
  [   11.215424]  proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
  [   11.216061]  vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
  [   11.218848]  ksys_write+0xfa/0x200 (fs/read_write.c:740)
  [   11.222394]  do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)
  [   11.223942]  entry_SYSCALL_64_after_hwframe+0x77/0x7f (arch/x86/entry/entry_64.S:121)

At build time, a new host tool (scripts/gen_lineinfo) reads DWARF
.debug_line from vmlinux using libdw (elfutils), extracts all
address-to-file:line mappings, and generates an assembly file with
sorted parallel arrays (offsets from _text, file IDs, and line
numbers). These are linked into vmlinux as .rodata.

At runtime, kallsyms_lookup_lineinfo() does a binary search on the
table and __sprint_symbol() appends "(file:line)" to each stack frame.
The lookup uses offsets from _text so it works with KASLR, requires no
locks or allocations, and is safe in any context including panic.

The feature requires CONFIG_DEBUG_INFO (for DWARF data) and
elfutils (libdw-dev) on the build host.

Memory footprint measured with a 1852-option x86_64 config:

  Table: 4,597,583 entries from 4,841 source files
    lineinfo_addrs[]     4,597,583 x u32  = 17.5 MiB
    lineinfo_file_ids[]  4,597,583 x u16  =  8.8 MiB
    lineinfo_lines[]     4,597,583 x u32  = 17.5 MiB
    file_offsets + filenames              ~  0.1 MiB
    Total .rodata increase:              ~ 44.0 MiB

  vmlinux (stripped):  529 MiB -> 573 MiB  (+44 MiB / +8.3%)

Note: this probably won't be something we roll into "production", but
it might be useful for the average user given the relatively low memory
footprint, in canary deployments for hyperscalers, or by default for
folks who run tests/fuzzing/etc.

Disclaimer: this was vibe coded over an afternoon with an AI coding
assistant.

The .config used for testing is a simple KVM guest configuration for
local development and testing.

Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
 Documentation/admin-guide/index.rst           |   1 +
 .../admin-guide/kallsyms-lineinfo.rst         |  72 +++
 MAINTAINERS                                   |   6 +
 include/linux/kallsyms.h                      |  17 +-
 init/Kconfig                                  |  20 +
 kernel/kallsyms.c                             |  56 ++
 kernel/kallsyms_internal.h                    |   9 +
 scripts/.gitignore                            |   1 +
 scripts/Makefile                              |   3 +
 scripts/empty_lineinfo.S                      |  30 ++
 scripts/gen_lineinfo.c                        | 510 ++++++++++++++++++
 scripts/kallsyms.c                            |  16 +
 scripts/link-vmlinux.sh                       |  43 +-
 13 files changed, 780 insertions(+), 4 deletions(-)
 create mode 100644 Documentation/admin-guide/kallsyms-lineinfo.rst
 create mode 100644 scripts/empty_lineinfo.S
 create mode 100644 scripts/gen_lineinfo.c

diff --git a/Documentation/admin-guide/index.rst b/Documentation/admin-guide/index.rst
index b734f8a2a2c48..1801b9880aeb7 100644
--- a/Documentation/admin-guide/index.rst
+++ b/Documentation/admin-guide/index.rst
@@ -73,6 +73,7 @@ problems and bugs in particular.
    ramoops
    dynamic-debug-howto
    init
+   kallsyms-lineinfo
    kdump/index
    perf/index
    pstore-blk
diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
new file mode 100644
index 0000000000000..c8ec124394354
--- /dev/null
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -0,0 +1,72 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+====================================
+Kallsyms Source Line Info (LINEINFO)
+====================================
+
+Overview
+========
+
+``CONFIG_KALLSYMS_LINEINFO`` embeds DWARF-derived source file and line number
+mappings into the kernel image so that stack traces include
+``(file.c:123)`` annotations next to each symbol.  This makes it significantly
+easier to pinpoint the exact source location during debugging, without needing
+to manually cross-reference addresses with ``addr2line``.
+
+Enabling the Feature
+====================
+
+Enable the following kernel configuration options::
+
+    CONFIG_KALLSYMS=y
+    CONFIG_DEBUG_INFO=y
+    CONFIG_KALLSYMS_LINEINFO=y
+
+Build dependency: the host tool ``scripts/gen_lineinfo`` requires ``libdw``
+from elfutils.  Install the development package:
+
+- Debian/Ubuntu: ``apt install libdw-dev``
+- Fedora/RHEL: ``dnf install elfutils-devel``
+- Arch Linux: ``pacman -S elfutils``
+
+Example Output
+==============
+
+Without ``CONFIG_KALLSYMS_LINEINFO``::
+
+    Call Trace:
+     <TASK>
+     dump_stack_lvl+0x5d/0x80
+     do_syscall_64+0x82/0x190
+     entry_SYSCALL_64_after_hwframe+0x76/0x7e
+
+With ``CONFIG_KALLSYMS_LINEINFO``::
+
+    Call Trace:
+     <TASK>
+     dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:123)
+     do_syscall_64+0x82/0x190 (arch/x86/entry/common.c:52)
+     entry_SYSCALL_64_after_hwframe+0x76/0x7e
+
+Note that assembly routines (such as ``entry_SYSCALL_64_after_hwframe``) are
+not annotated because they lack DWARF debug information.
+
+Memory Overhead
+===============
+
+The lineinfo tables are stored in ``.rodata`` and typically add approximately
+44 MiB to the kernel image for a standard configuration (~4.6 million DWARF
+line entries, ~10 bytes per entry after deduplication).
+
+Known Limitations
+=================
+
+- **vmlinux only**: Only symbols in the core kernel image are annotated.
+  Module symbols are not covered.
+- **4 GiB offset limit**: Address offsets from ``_text`` are stored as 32-bit
+  values.  Entries beyond 4 GiB from ``_text`` are skipped at build time with
+  a warning.
+- **65535 file limit**: Source file IDs are stored as 16-bit values.  Builds
+  with more than 65535 unique source files will fail with an error.
+- **No assembly annotations**: Functions implemented in assembly that lack
+  DWARF ``.debug_line`` data are not annotated.
diff --git a/MAINTAINERS b/MAINTAINERS
index 61bf550fd37c2..f061e69b6e32a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13728,6 +13728,12 @@ S:	Maintained
 F:	Documentation/hwmon/k8temp.rst
 F:	drivers/hwmon/k8temp.c
 
+KALLSYMS LINEINFO
+M:	Sasha Levin <sashal@kernel.org>
+S:	Maintained
+F:	Documentation/admin-guide/kallsyms-lineinfo.rst
+F:	scripts/gen_lineinfo.c
+
 KASAN
 M:	Andrey Ryabinin <ryabinin.a.a@gmail.com>
 R:	Alexander Potapenko <glider@google.com>
diff --git a/include/linux/kallsyms.h b/include/linux/kallsyms.h
index d5dd54c53ace6..7d4c9dca06c87 100644
--- a/include/linux/kallsyms.h
+++ b/include/linux/kallsyms.h
@@ -16,10 +16,15 @@
 #include <asm/sections.h>
 
 #define KSYM_NAME_LEN 512
+
+/* Extra space for " (path/to/file.c:12345)" suffix when lineinfo is enabled */
+#define KSYM_LINEINFO_LEN (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) ? 128 : 0)
+
 #define KSYM_SYMBOL_LEN (sizeof("%s+%#lx/%#lx [%s %s]") + \
 			(KSYM_NAME_LEN - 1) + \
 			2*(BITS_PER_LONG*3/10) + (MODULE_NAME_LEN - 1) + \
-			(BUILD_ID_SIZE_MAX * 2) + 1)
+			(BUILD_ID_SIZE_MAX * 2) + 1 + \
+			KSYM_LINEINFO_LEN)
 
 struct cred;
 struct module;
@@ -96,6 +101,9 @@ extern int sprint_backtrace_build_id(char *buffer, unsigned long address);
 
 int lookup_symbol_name(unsigned long addr, char *symname);
 
+bool kallsyms_lookup_lineinfo(unsigned long addr,
+			      const char **file, unsigned int *line);
+
 #else /* !CONFIG_KALLSYMS */
 
 static inline unsigned long kallsyms_lookup_name(const char *name)
@@ -164,6 +172,13 @@ static inline int kallsyms_on_each_match_symbol(int (*fn)(void *, unsigned long)
 {
 	return -EOPNOTSUPP;
 }
+
+static inline bool kallsyms_lookup_lineinfo(unsigned long addr,
+					    const char **file,
+					    unsigned int *line)
+{
+	return false;
+}
 #endif /*CONFIG_KALLSYMS*/
 
 static inline void print_ip_sym(const char *loglvl, unsigned long ip)
diff --git a/init/Kconfig b/init/Kconfig
index b55deae9256c7..c39f27e6393a8 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2050,6 +2050,26 @@ config KALLSYMS_ALL
 
 	  Say N unless you really need all symbols, or kernel live patching.
 
+config KALLSYMS_LINEINFO
+	bool "Embed source file:line information in stack traces"
+	depends on KALLSYMS && DEBUG_INFO
+	help
+	  Embeds an address-to-source-line mapping table in the kernel
+	  image so that stack traces directly include file:line information,
+	  similar to what scripts/decode_stacktrace.sh provides but without
+	  needing external tools or a vmlinux with debug info at runtime.
+
+	  When enabled, stack traces will look like:
+
+	    kmem_cache_alloc_noprof+0x60/0x630 (mm/slub.c:3456)
+	    anon_vma_clone+0x2ed/0xcf0 (mm/rmap.c:412)
+
+	  This requires elfutils (libdw-dev/elfutils-devel) on the build host.
+	  Adds approximately 44MB to a typical kernel image (10 bytes per
+	  DWARF line-table entry, ~4.6M entries for a typical config).
+
+	  If unsure, say N.
+
 # end of the "standard kernel features (expert users)" menu
 
 config ARCH_HAS_MEMBARRIER_CALLBACKS
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index aec2f06858afd..d0a9cd9c6dace 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -467,6 +467,52 @@ static int append_buildid(char *buffer,   const char *modname,
 
 #endif /* CONFIG_STACKTRACE_BUILD_ID */
 
+bool kallsyms_lookup_lineinfo(unsigned long addr,
+			      const char **file, unsigned int *line)
+{
+	unsigned long long raw_offset;
+	unsigned int offset, low, high, mid, file_id;
+
+	if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) || !lineinfo_num_entries)
+		return false;
+
+	/* Compute offset from _text */
+	if (addr < (unsigned long)_text)
+		return false;
+
+	raw_offset = addr - (unsigned long)_text;
+	if (raw_offset > UINT_MAX)
+		return false;
+	offset = (unsigned int)raw_offset;
+
+	/* Binary search for largest entry <= offset */
+	low = 0;
+	high = lineinfo_num_entries;
+	while (low < high) {
+		mid = low + (high - low) / 2;
+		if (lineinfo_addrs[mid] <= offset)
+			low = mid + 1;
+		else
+			high = mid;
+	}
+
+	if (low == 0)
+		return false;
+	low--;
+
+	file_id = lineinfo_file_ids[low];
+	*line = lineinfo_lines[low];
+
+	if (file_id >= lineinfo_num_files)
+		return false;
+
+	if (lineinfo_file_offsets[file_id] >= lineinfo_filenames_size)
+		return false;
+
+	*file = &lineinfo_filenames[lineinfo_file_offsets[file_id]];
+	return true;
+}
+
 /* Look up a kernel symbol and return it in a text buffer. */
 static int __sprint_symbol(char *buffer, unsigned long address,
 			   int symbol_offset, int add_offset, int add_buildid)
@@ -497,6 +543,16 @@ static int __sprint_symbol(char *buffer, unsigned long address,
 		len += sprintf(buffer + len, "]");
 	}
 
+	if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) && !modname) {
+		const char *li_file;
+		unsigned int li_line;
+
+		if (kallsyms_lookup_lineinfo(address,
+					     &li_file, &li_line))
+			len += snprintf(buffer + len, KSYM_SYMBOL_LEN - len,
+					" (%s:%u)", li_file, li_line);
+	}
+
 	return len;
 }
 
diff --git a/kernel/kallsyms_internal.h b/kernel/kallsyms_internal.h
index 81a867dbe57d4..d7374ce444d81 100644
--- a/kernel/kallsyms_internal.h
+++ b/kernel/kallsyms_internal.h
@@ -15,4 +15,13 @@ extern const u16 kallsyms_token_index[];
 extern const unsigned int kallsyms_markers[];
 extern const u8 kallsyms_seqs_of_names[];
 
+extern const u32 lineinfo_num_entries;
+extern const u32 lineinfo_addrs[];
+extern const u16 lineinfo_file_ids[];
+extern const u32 lineinfo_lines[];
+extern const u32 lineinfo_num_files;
+extern const u32 lineinfo_file_offsets[];
+extern const u32 lineinfo_filenames_size;
+extern const char lineinfo_filenames[];
+
 #endif // LINUX_KALLSYMS_INTERNAL_H_
diff --git a/scripts/.gitignore b/scripts/.gitignore
index 4215c2208f7e4..e175714c18b61 100644
--- a/scripts/.gitignore
+++ b/scripts/.gitignore
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: GPL-2.0-only
 /asn1_compiler
+/gen_lineinfo
 /gen_packed_field_checks
 /generate_rust_target
 /insert-sys-cert
diff --git a/scripts/Makefile b/scripts/Makefile
index 0941e5ce7b575..ffe89875b3295 100644
--- a/scripts/Makefile
+++ b/scripts/Makefile
@@ -4,6 +4,7 @@
 # the kernel for the build process.
 
 hostprogs-always-$(CONFIG_KALLSYMS)			+= kallsyms
+hostprogs-always-$(CONFIG_KALLSYMS_LINEINFO)		+= gen_lineinfo
 hostprogs-always-$(BUILD_C_RECORDMCOUNT)		+= recordmcount
 hostprogs-always-$(CONFIG_BUILDTIME_TABLE_SORT)		+= sorttable
 hostprogs-always-$(CONFIG_ASN1)				+= asn1_compiler
@@ -36,6 +37,8 @@ HOSTLDLIBS_sorttable = -lpthread
 HOSTCFLAGS_asn1_compiler.o = -I$(srctree)/include
 HOSTCFLAGS_sign-file.o = $(shell $(HOSTPKG_CONFIG) --cflags libcrypto 2> /dev/null)
 HOSTLDLIBS_sign-file = $(shell $(HOSTPKG_CONFIG) --libs libcrypto 2> /dev/null || echo -lcrypto)
+HOSTCFLAGS_gen_lineinfo.o = $(shell $(HOSTPKG_CONFIG) --cflags libdw 2> /dev/null)
+HOSTLDLIBS_gen_lineinfo = $(shell $(HOSTPKG_CONFIG) --libs libdw 2> /dev/null || echo -ldw -lelf -lz)
 
 ifdef CONFIG_UNWINDER_ORC
 ifeq ($(ARCH),x86_64)
diff --git a/scripts/empty_lineinfo.S b/scripts/empty_lineinfo.S
new file mode 100644
index 0000000000000..e058c41137123
--- /dev/null
+++ b/scripts/empty_lineinfo.S
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Empty lineinfo stub for the initial vmlinux link.
+ * The real lineinfo is generated from .tmp_vmlinux1 by gen_lineinfo.
+ */
+	.section .rodata, "a"
+	.globl lineinfo_num_entries
+	.balign 4
+lineinfo_num_entries:
+	.long 0
+	.globl lineinfo_num_files
+	.balign 4
+lineinfo_num_files:
+	.long 0
+	.globl lineinfo_addrs
+lineinfo_addrs:
+	.globl lineinfo_file_ids
+lineinfo_file_ids:
+	.globl lineinfo_lines
+lineinfo_lines:
+	.globl lineinfo_file_offsets
+lineinfo_file_offsets:
+	.globl lineinfo_filenames_size
+	.balign 4
+lineinfo_filenames_size:
+	.long 0
+	.globl lineinfo_filenames
+lineinfo_filenames:
diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
new file mode 100644
index 0000000000000..37d5e84971be4
--- /dev/null
+++ b/scripts/gen_lineinfo.c
@@ -0,0 +1,510 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * gen_lineinfo.c - Generate address-to-source-line lookup tables from DWARF
+ *
+ * Copyright (C) 2026 Sasha Levin <sashal@kernel.org>
+ *
+ * Reads DWARF .debug_line from a vmlinux ELF file and outputs an assembly
+ * file containing sorted lookup tables that the kernel uses to annotate
+ * stack traces with source file:line information.
+ *
+ * Requires libdw from elfutils.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <elfutils/libdw.h>
+#include <dwarf.h>
+#include <elf.h>
+#include <gelf.h>
+#include <limits.h>
+
+static unsigned int skipped_overflow;
+
+struct line_entry {
+	unsigned int offset;	/* offset from _text */
+	unsigned int file_id;
+	unsigned int line;
+};
+
+struct file_entry {
+	char *name;
+	unsigned int id;
+	unsigned int str_offset;
+};
+
+static struct line_entry *entries;
+static unsigned int num_entries;
+static unsigned int entries_capacity;
+
+static struct file_entry *files;
+static unsigned int num_files;
+static unsigned int files_capacity;
+
+#define FILE_HASH_BITS 13
+#define FILE_HASH_SIZE (1 << FILE_HASH_BITS)
+
+struct file_hash_entry {
+	const char *name;
+	unsigned int id;
+};
+
+static struct file_hash_entry file_hash[FILE_HASH_SIZE];
+
+static unsigned int hash_str(const char *s)
+{
+	unsigned int h = 5381;
+
+	for (; *s; s++)
+		h = h * 33 + (unsigned char)*s;
+	return h & (FILE_HASH_SIZE - 1);
+}
+
+static void add_entry(unsigned int offset, unsigned int file_id,
+		      unsigned int line)
+{
+	if (num_entries >= entries_capacity) {
+		entries_capacity = entries_capacity ? entries_capacity * 2 : 65536;
+		entries = realloc(entries, entries_capacity * sizeof(*entries));
+		if (!entries) {
+			fprintf(stderr, "out of memory\n");
+			exit(1);
+		}
+	}
+	entries[num_entries].offset = offset;
+	entries[num_entries].file_id = file_id;
+	entries[num_entries].line = line;
+	num_entries++;
+}
+
+static unsigned int find_or_add_file(const char *name)
+{
+	unsigned int h = hash_str(name);
+
+	/* Open-addressing lookup with linear probing */
+	while (file_hash[h].name) {
+		if (!strcmp(file_hash[h].name, name))
+			return file_hash[h].id;
+		h = (h + 1) & (FILE_HASH_SIZE - 1);
+	}
+
+	if (num_files >= 65535) {
+		fprintf(stderr,
+			"gen_lineinfo: too many source files (%u > 65535)\n",
+			num_files);
+		exit(1);
+	}
+
+	if (num_files >= files_capacity) {
+		files_capacity = files_capacity ? files_capacity * 2 : 4096;
+		files = realloc(files, files_capacity * sizeof(*files));
+		if (!files) {
+			fprintf(stderr, "out of memory\n");
+			exit(1);
+		}
+	}
+	files[num_files].name = strdup(name);
+	files[num_files].id = num_files;
+
+	/* Insert into hash table (points to files[] entry) */
+	file_hash[h].name = files[num_files].name;
+	file_hash[h].id = num_files;
+
+	num_files++;
+	return num_files - 1;
+}
+
+/*
+ * Well-known top-level directories in the kernel source tree.
+ * Used as a fallback to recover relative paths from absolute DWARF paths
+ * when comp_dir doesn't match (e.g. O= out-of-tree builds where comp_dir
+ * is the build directory but source paths point into the source tree).
+ */
+static const char * const kernel_dirs[] = {
+	"arch/", "block/", "certs/", "crypto/", "drivers/", "fs/",
+	"include/", "init/", "io_uring/", "ipc/", "kernel/", "lib/",
+	"mm/", "net/", "rust/", "samples/", "scripts/", "security/",
+	"sound/", "tools/", "usr/", "virt/",
+};
+
+/*
+ * Strip a filename to a kernel-relative path.
+ *
+ * For absolute paths, strip the comp_dir prefix (from DWARF) to get
+ * a kernel-tree-relative path.  When that fails (e.g. O= builds where
+ * comp_dir is the build directory), scan for a well-known kernel
+ * top-level directory name in the path to recover the relative path.
+ * Fall back to the basename as a last resort.
+ *
+ * For relative paths (common in modules), libdw may produce a bogus
+ * doubled path like "net/foo/bar.c/net/foo/bar.c" due to ET_REL DWARF
+ * quirks.  Detect and strip such duplicates.
+ */
+static const char *make_relative(const char *path, const char *comp_dir)
+{
+	const char *p;
+
+	/* If already relative, use as-is */
+	if (path[0] != '/')
+		return path;
+
+	/* comp_dir from DWARF is the most reliable method */
+	if (comp_dir) {
+		size_t len = strlen(comp_dir);
+
+		if (!strncmp(path, comp_dir, len) && path[len] == '/') {
+			const char *rel = path + len + 1;
+
+			/*
+			 * If comp_dir pointed to a subdirectory
+			 * (e.g. arch/parisc/kernel) rather than
+			 * the tree root, stripping it leaves a
+			 * bare filename.  Fall through to the
+			 * kernel_dirs scan so we recover the full
+			 * relative path instead.
+			 */
+			if (strchr(rel, '/'))
+				return rel;
+		}
+
+		/*
+		 * comp_dir prefix didn't help — either it didn't match
+		 * or it was too specific and left a bare filename.
+		 * Scan for a known kernel top-level directory component
+		 * to find where the relative path starts.  This handles
+		 * O= builds and arches where comp_dir is a subdirectory.
+		 */
+		for (p = path + 1; *p; p++) {
+			if (*(p - 1) == '/') {
+				for (unsigned int i = 0; i < sizeof(kernel_dirs) /
+				     sizeof(kernel_dirs[0]); i++) {
+					if (!strncmp(p, kernel_dirs[i],
+						     strlen(kernel_dirs[i])))
+						return p;
+				}
+			}
+		}
+
+		/* Fall back to basename */
+		p = strrchr(path, '/');
+		return p ? p + 1 : path;
+	}
+
+	/* Fall back to basename */
+	p = strrchr(path, '/');
+	return p ? p + 1 : path;
+}
+
+static int compare_entries(const void *a, const void *b)
+{
+	const struct line_entry *ea = a;
+	const struct line_entry *eb = b;
+
+	if (ea->offset != eb->offset)
+		return ea->offset < eb->offset ? -1 : 1;
+	if (ea->file_id != eb->file_id)
+		return ea->file_id < eb->file_id ? -1 : 1;
+	if (ea->line != eb->line)
+		return ea->line < eb->line ? -1 : 1;
+	return 0;
+}
+
+static unsigned long long find_text_addr(Elf *elf)
+{
+	size_t nsyms, i;
+	Elf_Scn *scn = NULL;
+	GElf_Shdr shdr;
+
+	while ((scn = elf_nextscn(elf, scn)) != NULL) {
+		Elf_Data *data;
+
+		if (!gelf_getshdr(scn, &shdr))
+			continue;
+		if (shdr.sh_type != SHT_SYMTAB)
+			continue;
+
+		data = elf_getdata(scn, NULL);
+		if (!data)
+			continue;
+
+		nsyms = shdr.sh_size / shdr.sh_entsize;
+		for (i = 0; i < nsyms; i++) {
+			GElf_Sym sym;
+			const char *name;
+
+			if (!gelf_getsym(data, i, &sym))
+				continue;
+			name = elf_strptr(elf, shdr.sh_link, sym.st_name);
+			if (name && !strcmp(name, "_text"))
+				return sym.st_value;
+		}
+	}
+
+	fprintf(stderr, "Cannot find _text symbol\n");
+	exit(1);
+}
+
+static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
+{
+	Dwarf_Off off = 0, next_off;
+	size_t hdr_size;
+
+	while (dwarf_nextcu(dwarf, off, &next_off, &hdr_size,
+			    NULL, NULL, NULL) == 0) {
+		Dwarf_Die cudie;
+		Dwarf_Lines *lines;
+		size_t nlines;
+		Dwarf_Attribute attr;
+		const char *comp_dir = NULL;
+
+		if (!dwarf_offdie(dwarf, off + hdr_size, &cudie))
+			goto next;
+
+		if (dwarf_attr(&cudie, DW_AT_comp_dir, &attr))
+			comp_dir = dwarf_formstring(&attr);
+
+		if (dwarf_getsrclines(&cudie, &lines, &nlines) != 0)
+			goto next;
+
+		for (size_t i = 0; i < nlines; i++) {
+			Dwarf_Line *line = dwarf_onesrcline(lines, i);
+			Dwarf_Addr addr;
+			const char *src;
+			const char *rel;
+			unsigned int file_id, loffset;
+			int lineno;
+
+			if (!line)
+				continue;
+
+			if (dwarf_lineaddr(line, &addr) != 0)
+				continue;
+			if (dwarf_lineno(line, &lineno) != 0)
+				continue;
+			if (lineno == 0)
+				continue;
+
+			src = dwarf_linesrc(line, NULL, NULL);
+			if (!src)
+				continue;
+
+			if (addr < text_addr)
+				continue;
+
+			{
+				unsigned long long raw_offset = addr - text_addr;
+
+				if (raw_offset > UINT_MAX) {
+					skipped_overflow++;
+					continue;
+				}
+				loffset = (unsigned int)raw_offset;
+			}
+
+			rel = make_relative(src, comp_dir);
+			file_id = find_or_add_file(rel);
+
+			add_entry(loffset, file_id, (unsigned int)lineno);
+		}
+next:
+		off = next_off;
+	}
+}
+
+static void deduplicate(void)
+{
+	unsigned int i, j;
+
+	if (num_entries < 2)
+		return;
+
+	/* Sort by offset, then file_id, then line for stability */
+	qsort(entries, num_entries, sizeof(*entries), compare_entries);
+
+	/*
+	 * Remove duplicate entries:
+	 * - Same offset: keep first (deterministic from stable sort keys)
+	 * - Same file:line as previous kept entry: redundant for binary
+	 *   search -- any address between them resolves to the earlier one
+	 */
+	j = 0;
+	for (i = 1; i < num_entries; i++) {
+		if (entries[i].offset == entries[j].offset)
+			continue;
+		if (entries[i].file_id == entries[j].file_id &&
+		    entries[i].line == entries[j].line)
+			continue;
+		j++;
+		if (j != i)
+			entries[j] = entries[i];
+	}
+	num_entries = j + 1;
+}
+
+static void compute_file_offsets(void)
+{
+	unsigned int offset = 0;
+
+	for (unsigned int i = 0; i < num_files; i++) {
+		files[i].str_offset = offset;
+		offset += strlen(files[i].name) + 1;
+	}
+}
+
+static void print_escaped_asciz(const char *s)
+{
+	printf("\t.asciz \"");
+	for (; *s; s++) {
+		if (*s == '"' || *s == '\\')
+			putchar('\\');
+		putchar(*s);
+	}
+	printf("\"\n");
+}
+
+static void output_assembly(void)
+{
+	printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
+	printf("/*\n");
+	printf(" * Automatically generated by scripts/gen_lineinfo\n");
+	printf(" * Do not edit.\n");
+	printf(" */\n\n");
+
+	printf("\t.section .rodata, \"a\"\n\n");
+
+	/* Number of entries */
+	printf("\t.globl lineinfo_num_entries\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_num_entries:\n");
+	printf("\t.long %u\n\n", num_entries);
+
+	/* Number of files */
+	printf("\t.globl lineinfo_num_files\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_num_files:\n");
+	printf("\t.long %u\n\n", num_files);
+
+	/* Sorted address offsets from _text */
+	printf("\t.globl lineinfo_addrs\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_addrs:\n");
+	for (unsigned int i = 0; i < num_entries; i++)
+		printf("\t.long 0x%x\n", entries[i].offset);
+	printf("\n");
+
+	/* File IDs, parallel to addrs (u16 -- supports up to 65535 files) */
+	printf("\t.globl lineinfo_file_ids\n");
+	printf("\t.balign 2\n");
+	printf("lineinfo_file_ids:\n");
+	for (unsigned int i = 0; i < num_entries; i++)
+		printf("\t.short %u\n", entries[i].file_id);
+	printf("\n");
+
+	/* Line numbers, parallel to addrs */
+	printf("\t.globl lineinfo_lines\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_lines:\n");
+	for (unsigned int i = 0; i < num_entries; i++)
+		printf("\t.long %u\n", entries[i].line);
+	printf("\n");
+
+	/* File string offset table */
+	printf("\t.globl lineinfo_file_offsets\n");
+	printf("\t.balign 4\n");
+	printf("lineinfo_file_offsets:\n");
+	for (unsigned int i = 0; i < num_files; i++)
+		printf("\t.long %u\n", files[i].str_offset);
+	printf("\n");
+
+	/* Filenames size */
+	{
+		unsigned int fsize = 0;
+
+		for (unsigned int i = 0; i < num_files; i++)
+			fsize += strlen(files[i].name) + 1;
+		printf("\t.globl lineinfo_filenames_size\n");
+		printf("\t.balign 4\n");
+		printf("lineinfo_filenames_size:\n");
+		printf("\t.long %u\n\n", fsize);
+	}
+
+	/* Concatenated NUL-terminated filenames */
+	printf("\t.globl lineinfo_filenames\n");
+	printf("lineinfo_filenames:\n");
+	for (unsigned int i = 0; i < num_files; i++)
+		print_escaped_asciz(files[i].name);
+	printf("\n");
+}
+
+int main(int argc, char *argv[])
+{
+	int fd;
+	Elf *elf;
+	Dwarf *dwarf;
+	unsigned long long text_addr;
+
+	if (argc != 2) {
+		fprintf(stderr, "Usage: %s <vmlinux>\n", argv[0]);
+		return 1;
+	}
+
+	fd = open(argv[1], O_RDONLY);
+	if (fd < 0) {
+		fprintf(stderr, "Cannot open %s: %s\n", argv[1],
+			strerror(errno));
+		return 1;
+	}
+
+	elf_version(EV_CURRENT);
+	elf = elf_begin(fd, ELF_C_READ, NULL);
+	if (!elf) {
+		fprintf(stderr, "elf_begin failed: %s\n",
+			elf_errmsg(elf_errno()));
+		close(fd);
+		return 1;
+	}
+
+	text_addr = find_text_addr(elf);
+
+	dwarf = dwarf_begin_elf(elf, DWARF_C_READ, NULL);
+	if (!dwarf) {
+		fprintf(stderr, "dwarf_begin_elf failed: %s\n",
+			dwarf_errmsg(dwarf_errno()));
+		fprintf(stderr, "Is %s built with CONFIG_DEBUG_INFO?\n",
+			argv[1]);
+		elf_end(elf);
+		close(fd);
+		return 1;
+	}
+
+	process_dwarf(dwarf, text_addr);
+
+	if (skipped_overflow)
+		fprintf(stderr,
+			"lineinfo: warning: %u entries skipped (offset > 4 GiB from _text)\n",
+			skipped_overflow);
+
+	deduplicate();
+	compute_file_offsets();
+
+	fprintf(stderr, "lineinfo: %u entries, %u files\n",
+		num_entries, num_files);
+
+	output_assembly();
+
+	dwarf_end(dwarf);
+	elf_end(elf);
+	close(fd);
+
+	/* Cleanup */
+	free(entries);
+	for (unsigned int i = 0; i < num_files; i++)
+		free(files[i].name);
+	free(files);
+
+	return 0;
+}
diff --git a/scripts/kallsyms.c b/scripts/kallsyms.c
index 37d5c095ad22a..42662c4fbc6c9 100644
--- a/scripts/kallsyms.c
+++ b/scripts/kallsyms.c
@@ -78,6 +78,17 @@ static char *sym_name(const struct sym_entry *s)
 
 static bool is_ignored_symbol(const char *name, char type)
 {
+	/* Ignore lineinfo symbols for kallsyms pass stability */
+	static const char * const lineinfo_syms[] = {
+		"lineinfo_addrs",
+		"lineinfo_file_ids",
+		"lineinfo_file_offsets",
+		"lineinfo_filenames",
+		"lineinfo_lines",
+		"lineinfo_num_entries",
+		"lineinfo_num_files",
+	};
+
 	if (type == 'u' || type == 'n')
 		return true;
 
@@ -90,6 +101,11 @@ static bool is_ignored_symbol(const char *name, char type)
 			return true;
 	}
 
+	for (size_t i = 0; i < ARRAY_SIZE(lineinfo_syms); i++) {
+		if (!strcmp(name, lineinfo_syms[i]))
+			return true;
+	}
+
 	return false;
 }
 
diff --git a/scripts/link-vmlinux.sh b/scripts/link-vmlinux.sh
index f99e196abeea4..39ca44fbb259b 100755
--- a/scripts/link-vmlinux.sh
+++ b/scripts/link-vmlinux.sh
@@ -103,7 +103,7 @@ vmlinux_link()
 	${ld} ${ldflags} -o ${output}					\
 		${wl}--whole-archive ${objs} ${wl}--no-whole-archive	\
 		${wl}--start-group ${libs} ${wl}--end-group		\
-		${kallsymso} ${btf_vmlinux_bin_o} ${arch_vmlinux_o} ${ldlibs}
+		${kallsymso} ${lineinfo_o} ${btf_vmlinux_bin_o} ${arch_vmlinux_o} ${ldlibs}
 }
 
 # Create ${2}.o file with all symbols from the ${1} object file
@@ -129,6 +129,26 @@ kallsyms()
 	kallsymso=${2}.o
 }
 
+# Generate lineinfo tables from DWARF debug info in a temporary vmlinux.
+# ${1} - temporary vmlinux with debug info
+# Output: sets lineinfo_o to the generated .o file
+gen_lineinfo()
+{
+	info LINEINFO .tmp_lineinfo.S
+	if ! scripts/gen_lineinfo "${1}" > .tmp_lineinfo.S; then
+		echo >&2 "Failed to generate lineinfo from ${1}"
+		echo >&2 "Try to disable CONFIG_KALLSYMS_LINEINFO"
+		exit 1
+	fi
+
+	info AS .tmp_lineinfo.o
+	${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+	      ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \
+	      -c -o .tmp_lineinfo.o .tmp_lineinfo.S
+
+	lineinfo_o=.tmp_lineinfo.o
+}
+
 # Perform kallsyms for the given temporary vmlinux.
 sysmap_and_kallsyms()
 {
@@ -155,6 +175,7 @@ sorttable()
 cleanup()
 {
 	rm -f .btf.*
+	rm -f .tmp_lineinfo.*
 	rm -f .tmp_vmlinux.nm-sort
 	rm -f System.map
 	rm -f vmlinux
@@ -183,6 +204,7 @@ fi
 btf_vmlinux_bin_o=
 btfids_vmlinux=
 kallsymso=
+lineinfo_o=
 strip_debug=
 generate_map=
 
@@ -198,10 +220,21 @@ if is_enabled CONFIG_KALLSYMS; then
 	kallsyms .tmp_vmlinux0.syms .tmp_vmlinux0.kallsyms
 fi
 
+if is_enabled CONFIG_KALLSYMS_LINEINFO; then
+	# Assemble an empty lineinfo stub for the initial link.
+	# The real lineinfo is generated from .tmp_vmlinux1 by gen_lineinfo.
+	${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
+	      ${KBUILD_AFLAGS} ${KBUILD_AFLAGS_KERNEL} \
+	      -c -o .tmp_lineinfo.o "${srctree}/scripts/empty_lineinfo.S"
+	lineinfo_o=.tmp_lineinfo.o
+fi
+
 if is_enabled CONFIG_KALLSYMS || is_enabled CONFIG_DEBUG_INFO_BTF; then
 
-	# The kallsyms linking does not need debug symbols, but the BTF does.
-	if ! is_enabled CONFIG_DEBUG_INFO_BTF; then
+	# The kallsyms linking does not need debug symbols, but BTF and
+	# lineinfo generation do.
+	if ! is_enabled CONFIG_DEBUG_INFO_BTF &&
+	   ! is_enabled CONFIG_KALLSYMS_LINEINFO; then
 		strip_debug=1
 	fi
 
@@ -219,6 +252,10 @@ if is_enabled CONFIG_DEBUG_INFO_BTF; then
 	btfids_vmlinux=.tmp_vmlinux1.BTF_ids
 fi
 
+if is_enabled CONFIG_KALLSYMS_LINEINFO; then
+	gen_lineinfo .tmp_vmlinux1
+fi
+
 if is_enabled CONFIG_KALLSYMS; then
 
 	# kallsyms support
-- 
2.51.0


^ permalink raw reply related

* [PATCH v4 0/4] kallsyms: embed source file:line info in kernel stack traces
From: Sasha Levin @ 2026-03-22 13:15 UTC (permalink / raw)
  To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley
  Cc: Jonathan Corbet, Nathan Chancellor, Nicolas Schier, Petr Pavlu,
	Daniel Gomez, Greg KH, Petr Mladek, Steven Rostedt, Kees Cook,
	Peter Zijlstra, Thorsten Leemhuis, Vlastimil Babka, Helge Deller,
	Randy Dunlap, Laurent Pinchart, Vivian Wang, linux-kernel,
	linux-kbuild, linux-modules, linux-doc, Sasha Levin

This series adds CONFIG_KALLSYMS_LINEINFO, which embeds source file:line
information directly in the kernel image so that stack traces annotate
every frame with the originating source location - no external tools, no
debug symbols at runtime, and safe to use in NMI/panic context.

Motivation
==========

The recent "slowly decommission bugzilla?" thread surfaced a recurring
problem: when users encounter kernel crashes they see stack traces like
`func+0x1ec/0x240` but have no way to identify which subsystem or
maintainer to contact. Richard Weinberger proposed building a database
mapping symbols to source files using nm/DWARF. Linus pointed to
scripts/decode_stacktrace.sh as the existing solution. But as the
discussion progressed, it became clear that decode_stacktrace.sh has
significant practical barriers that prevent it from being useful in the
common case.

Problems with scripts/decode_stacktrace.sh
==========================================

- Requires debug symbols: the script needs vmlinux with DWARF debug
  info. Many distros don't retain debug symbols for older or security
  kernels, and even when available, asking users to obtain matching
  debuginfo packages is a significant hurdle.

- Requires toolchain: users need addr2line and nm installed.

- Version-matching requirement: debug symbols must exactly match the
  running kernel binary.

What this series does
=====================

Patch 1: CONFIG_KALLSYMS_LINEINFO

At build time, a host tool (scripts/gen_lineinfo) reads DWARF
.debug_line from vmlinux, extracts address-to-file:line mappings, and
embeds them as sorted lookup tables in .rodata. At runtime,
kallsyms_lookup_lineinfo() binary-searches the table and
__sprint_symbol() appends "(file:line)" to each stack frame. NMI/panic-
safe (no locks, no allocations), KASLR-compatible.

Patch 2: CONFIG_KALLSYMS_LINEINFO_MODULES

Extends lineinfo to loadable modules. Each .ko gets a .mod_lineinfo
section embedded at build time. The module loader picks it up at load
time. Same zero-allocation, NMI-safe lookup.

Patch 3: delta compression

Block-indexed delta-encoding with LEB128 varints, implementing the
approach suggested by Juergen Gross in the RFC review. Reduces overhead
from ~44 MiB to ~11 MiB (~3.7 bytes/entry), addressing the primary size
concern from the RFC.

Patch 4: KUnit tests

30 KUnit tests covering the lineinfo lookup paths, delta-decode logic,
boundary conditions, and integration with the backtrace formatting APIs.

Example output
==============

  [   11.206749]  dump_stack_lvl+0x5d/0x80 (lib/dump_stack.c:94)
  [   11.207403]  vpanic+0x36e/0x620 (kernel/panic.c:650)
  [   11.209324]  panic+0xc9/0xd0 (kernel/panic.c:787)
  [   11.213312]  sysrq_handle_crash+0x1a/0x20 (drivers/tty/sysrq.c:154)
  [   11.214005]  __handle_sysrq.cold+0x66/0x256 (drivers/tty/sysrq.c:611)
  [   11.214712]  write_sysrq_trigger+0x65/0x80 (drivers/tty/sysrq.c:1221)
  [   11.215424]  proc_reg_write+0x1bd/0x3c0 (fs/proc/inode.c:330)
  [   11.216061]  vfs_write+0x1c6/0xff0 (fs/read_write.c:686)
  [   11.218848]  ksys_write+0xfa/0x200 (fs/read_write.c:740)
  [   11.222394]  do_syscall_64+0xf3/0x690 (arch/x86/entry/syscall_64.c:63)

Size impact
===========

Measured with a Debian kernel config:

- bzImage: +3.6 MiB (14 MiB -> 18 MiB, +26%)
- Runtime memory: +5.9 MiB (text+data+bss)
- Code overhead: +5.0 KiB (.text, lookup functions only)
- Data overhead: +5.9 MiB (.data, lineinfo tables)

Lineinfo data breakdown:

- lineinfo_data (delta-compressed): 5,728 KiB (97%)
- lineinfo_block_addrs: 99 KiB
- lineinfo_block_offsets: 99 KiB
- lineinfo_filenames: 111 KiB
- lineinfo_file_offsets: 17 KiB

The ~5.9 MiB is after 2.7x delta compression; uncompressed would be
~16 MiB. This is a fraction of the cost of shipping full DWARF debug
info (hundreds of MiB), which distros must store and serve for every
kernel version.

For distros, maintaining debug symbol repositories is expensive: storage,
mirrors, and CDN bandwidth for hundreds of MiB per kernel build add up
quickly. A ~5.9 MiB increase in the kernel image itself is a modest cost
that eliminates the need for users to find, download, and version-match
debuginfo packages just to make a crash report useful.

For developers, the file:line annotations appear immediately in crash
traces - no post-processing with decode_stacktrace.sh needed.

Changes since v3
=================

- Remove redundant gen_lineinfo entry in scripts/Makefile for
  CONFIG_KALLSYMS_LINEINFO_MODULES (depends on CONFIG_KALLSYMS_LINEINFO
  which already builds it). (Reported by Petr Pavlu)

- Use R_* constants from <elf.h> instead of hardcoded relocation type
  values in r_type_abs32(). (Reported by Petr Pavlu)

- Simplify duplicated-path detection in make_relative(): replace loop
  over every '/' with a direct midpoint check, since true path
  duplication always splits at len/2. (Suggested by Petr Pavlu)

- Fix comment in process_dwarf(): sections in ET_REL objects have
  sh_addr == 0 and therefore overlapping address ranges; this is
  expected behavior, not a "may" situation. (Reported by Petr Pavlu)

- Use U32_MAX instead of UINT_MAX for the module raw_offset bounds
  check, matching the u32 type of the addrs array.
  (Reported by Petr Pavlu)

- Document the assumption that .text is at the start of the MOD_TEXT
  segment in module_lookup_lineinfo(). A proper fix using ELF
  relocations is planned for a future series.
  (Reported by Petr Pavlu)

- Wrap -fno-inline-functions-called-once in $(call cc-option,...) for
  clang compatibility. Clang does not support this GCC-specific flag;
  the noinline attribute is sufficient.

Changes since v2
=================

- Replace #ifdef CONFIG_KALLSYMS_LINEINFO with IS_ENABLED() throughout,
  so the compiler checks the code for syntax errors regardless of
  configuration. (Suggested by Helge Deller)

- Replace zigzag + ULEB128 encoding of signed deltas with native SLEB128,
  removing the unnecessary zigzag transform layer.
  (Suggested by Vivian Wang)

- Deduplicate the binary search and delta-decode logic: extract shared
  struct lineinfo_table and lineinfo_search() into mod_lineinfo.h
  instead of maintaining near-identical copies in kernel/kallsyms.c and
  kernel/module/kallsyms.c. (Suggested by Vivian Wang)

- Use .uleb128 / .sleb128 assembler directives in gen_lineinfo output
  instead of encoding varints in C and emitting .byte hex literals.
  (Suggested by Vivian Wang)

- Redesign module mod_lineinfo_header to use explicit (offset, size)
  pairs for each sub-array, similar to flattened devicetree layout.
  This makes bounds validation straightforward: offset + size <=
  section_size. (Suggested by Vivian Wang)

- Remove dead sym_start parameter from kallsyms_lookup_lineinfo() and
  module_lookup_lineinfo().

Changes since v1
=================

- Fix path stripping regression on architectures where DWARF comp_dir is
  a subdirectory (e.g. arch/parisc/kernel) rather than the source tree
  root: paths now correctly show "kernel/traps.c:212" instead of bare
  "traps.c:212". Added kernel_dirs[] fallback scan and bare-filename
  recovery via comp_dir. (Reported by Helge Deller)

- Fix RST heading: overline/underline must be at least as long as the
  heading text in kallsyms-lineinfo.rst. (Reported by Randy Dunlap)

- Fix MAINTAINERS alphabetical ordering: move KALLSYMS LINEINFO entry
  before KASAN. (Reported by Randy Dunlap)

- Fix arch-portability of .debug_line relocation handling: replace
  hardcoded R_X86_64_32 with r_type_abs32() supporting x86, arm, arm64,
  riscv, s390, mips, ppc, loongarch, and parisc.

- Fix vmlinux compressed-path data_end for the last block: use
  lineinfo_data_size instead of UINT_MAX.

- Add file_offsets[] and filenames_size bounds checks in vmlinux lookup
  path (the module path already had them).

- Add alignment padding for file_offsets[] in module .mod_lineinfo
  binary format (data[] is variable-length u8, followed by u32[]).

- Remove sym_start cross-validation check that incorrectly rejected
  valid lineinfo entries for assembly-adjacent functions.

- Add KUnit test suite (new patch 4/4): 30 tests covering vmlinux
  lookup, module lookup, delta decode, boundary conditions, and
  backtrace formatting integration.

Changes since RFC
==================

- Added module support (patch 2)
- Added delta compression (patch 3), reducing size from ~44 MiB to
  ~11 MiB, addressing the primary concern from RFC review
- Added documentation (Documentation/admin-guide/kallsyms-lineinfo.rst)
- Added MAINTAINERS entry

Sasha Levin (4):
  kallsyms: embed source file:line info in kernel stack traces
  kallsyms: extend lineinfo to loadable modules
  kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
  kallsyms: add KUnit tests for lineinfo feature

 Documentation/admin-guide/index.rst           |   1 +
 .../admin-guide/kallsyms-lineinfo.rst         |  97 ++
 MAINTAINERS                                   |   9 +
 include/linux/kallsyms.h                      |  17 +-
 include/linux/mod_lineinfo.h                  | 243 +++++
 include/linux/module.h                        |   5 +
 init/Kconfig                                  |  35 +
 kernel/kallsyms.c                             |  58 ++
 kernel/kallsyms_internal.h                    |  11 +
 kernel/module/kallsyms.c                      |  80 ++
 kernel/module/main.c                          |   3 +
 lib/Kconfig.debug                             |  10 +
 lib/tests/Makefile                            |   3 +
 lib/tests/lineinfo_kunit.c                    | 813 +++++++++++++++++
 scripts/.gitignore                            |   1 +
 scripts/Makefile                              |   3 +
 scripts/Makefile.modfinal                     |   6 +
 scripts/empty_lineinfo.S                      |  38 +
 scripts/gen-mod-lineinfo.sh                   |  48 +
 scripts/gen_lineinfo.c                        | 848 ++++++++++++++++++
 scripts/kallsyms.c                            |  17 +
 scripts/link-vmlinux.sh                       |  43 +-
 22 files changed, 2385 insertions(+), 4 deletions(-)
 create mode 100644 Documentation/admin-guide/kallsyms-lineinfo.rst
 create mode 100644 include/linux/mod_lineinfo.h
 create mode 100644 lib/tests/lineinfo_kunit.c
 create mode 100644 scripts/empty_lineinfo.S
 create mode 100755 scripts/gen-mod-lineinfo.sh
 create mode 100644 scripts/gen_lineinfo.c

-- 
2.51.0


^ permalink raw reply

* Re: [PATCH v3 0/8] module: Move 'struct module_signature' to UAPI
From: Nicolas Schier @ 2026-03-20 20:06 UTC (permalink / raw)
  To: Thomas Weißschuh
  Cc: David Howells, David Woodhouse, Luis Chamberlain, Petr Pavlu,
	Daniel Gomez, Sami Tolvanen, Aaron Tomlin, Heiko Carstens,
	Vasily Gorbik, Alexander Gordeev, Christian Borntraeger,
	Sven Schnelle, Mimi Zohar, Roberto Sassu, Dmitry Kasatkin,
	Eric Snowberg, Paul Moore, James Morris, Serge E. Hallyn,
	Nathan Chancellor, Alexei Starovoitov, Daniel Borkmann,
	Andrii Nakryiko, Martin KaFai Lau, Eduard Zingerman, Song Liu,
	Yonghong Song, John Fastabend, KP Singh, Stanislav Fomichev,
	Hao Luo, Jiri Olsa, Shuah Khan, keyrings, linux-kernel,
	linux-modules, linux-s390, linux-integrity, linux-security-module,
	linux-kbuild, bpf, linux-kselftest
In-Reply-To: <20260305-module-signature-uapi-v3-0-92f45ea6028c@linutronix.de>

[-- Attachment #1: Type: text/plain, Size: 2929 bytes --]

On Thu, Mar 05, 2026 at 10:31:36AM +0100, Thomas Weißschuh wrote:
> This structure definition is used outside the kernel proper.
> For example in kmod and the kernel build environment.
> 
> To allow reuse, move it to a new UAPI header.
> 
> While it is not a true UAPI, it is a common practice to have
> non-UAPI interface definitions in the kernel's UAPI headers.
> 
> This came up as part of my CONFIG_MODULE_HASHES series [0].
> But it is useful on its own and so we get it out of the way.
> 
> [0] https://lore.kernel.org/lkml/aZ3OfJJSJgfOb0rJ@levanger/
> 
> Signed-off-by: Thomas Weißschuh <thomas.weissschuh@linutronix.de>
> ---
> Changes in v3:
> - Also adapt the include path for the custom sign-file rule in the bpf selftests.
>   (My manual run of BPF CI still fails, due to an BUG() on s390,
>   I don't see how this is due to this patch)
> - Link to v2: https://lore.kernel.org/r/20260305-module-signature-uapi-v2-0-dc4d81129dee@linutronix.de
> 
> Changes in v2:
> - Drop spurious definition of MODULE_SIGNATURE_TYPE_MERKLE.
> - s/modules/module/ in two patch subjects.
> - Pick up review tags.
> - Link to v1: https://lore.kernel.org/r/20260302-module-signature-uapi-v1-0-207d955e0d69@linutronix.de
> 
> ---
> Thomas Weißschuh (8):
>       extract-cert: drop unused definition of PKEY_ID_PKCS7
>       module: Drop unused signature types
>       module: Give 'enum pkey_id_type' a more specific name
>       module: Give MODULE_SIG_STRING a more descriptive name
>       module: Move 'struct module_signature' to UAPI
>       tools uapi headers: add linux/module_signature.h
>       sign-file: use 'struct module_signature' from the UAPI headers
>       selftests/bpf: verify_pkcs7_sig: Use 'struct module_signature' from the UAPI headers
> 
>  arch/s390/kernel/machine_kexec_file.c              |  6 ++--
>  certs/extract-cert.c                               |  2 --
>  include/linux/module_signature.h                   | 30 +---------------
>  include/uapi/linux/module_signature.h              | 41 ++++++++++++++++++++++
>  kernel/module/signing.c                            |  4 +--
>  kernel/module_signature.c                          |  2 +-
>  scripts/Makefile                                   |  1 +
>  scripts/sign-file.c                                | 19 +++-------
>  security/integrity/ima/ima_modsig.c                |  6 ++--
>  tools/include/uapi/linux/module_signature.h        | 41 ++++++++++++++++++++++
>  tools/testing/selftests/bpf/Makefile               |  1 +
>  .../selftests/bpf/prog_tests/verify_pkcs7_sig.c    | 28 ++-------------
>  12 files changed, 101 insertions(+), 80 deletions(-)
> ---
> base-commit: 6de23f81a5e08be8fbf5e8d7e9febc72a5b5f27f
> change-id: 20260302-module-signature-uapi-61fa80b1e2bb
> 

Thanks for these patches!

For the whole series:

Reviewed-by: Nicolas Schier <nsc@kernel.org>

-- 
Nicolas

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]

^ permalink raw reply

* Re: [PATCH v2 0/2] module: expose imported namespaces via sysfs
From: Sami Tolvanen @ 2026-03-20 17:45 UTC (permalink / raw)
  To: Luis Chamberlain, Petr Pavlu, Daniel Gomez, Nicholas Sielicki
  Cc: Aaron Tomlin, Matthias Maennich, Peter Zijlstra, Jonathan Corbet,
	Shuah Khan, Randy Dunlap, linux-modules, linux-doc, linux-kernel
In-Reply-To: <20260307090010.20828-1-linux@opensource.nslick.com>

On Sat, 07 Mar 2026 03:00:08 -0600, Nicholas Sielicki wrote:
> Add /sys/module/*/import_ns to expose the symbol namespaces imported
> by a loaded module.
> 
> Changes since v1:
> - Simplified commit message to drop unnecessary/incorrect background
> - Use .setup/.free callbacks in module_attribute to ensure
>   imported_namespaces is NULL-initialized before error paths and
>   NULL'd after kfree (Sami)
> - Updated KernelVersion to 7.1 in docs for next merge window
> 
> [...]

Applied to modules-next, thanks!

[1/2] module: expose imported namespaces via sysfs
      commit: 3fe1dcbc2d20c5dbc581c0bb458e05365bfffcf7
[2/2] docs: symbol-namespaces: mention sysfs attribute
      commit: f15dbe8a94b6e3768b10e10bf8ab95b28682db80

Best regards,

	Sami



^ permalink raw reply

* Re: [PATCH v2 0/3] module: Fix freeing of charp module parameters when CONFIG_SYSFS=n
From: Sami Tolvanen @ 2026-03-20 17:45 UTC (permalink / raw)
  To: Luis Chamberlain, Daniel Gomez, Petr Pavlu
  Cc: Christophe Leroy, Aaron Tomlin, Greg Kroah-Hartman,
	Rafael J. Wysocki, Danilo Krummrich, linux-modules, linux-kernel
In-Reply-To: <20260313134932.335275-1-petr.pavlu@suse.com>

On Fri, 13 Mar 2026 14:48:01 +0100, Petr Pavlu wrote:
> Fix freeing of charp module parameters when CONFIG_SYSFS=n and, related to
> this, update moduleparam.h to keep its coding style consistent.
> 
> Changes since v1 [1]:
> * Remove the extern keyword from the declaration of module_destroy_params()
>   and update the type of its num parameter from `unsigned` to
>   `unsigned int`.
> * Add a cleanup patch for parse_args() to similarly update its num
>   parameter to `unsigned int` and to synchronize the parameter names
>   between its prototype and definition.
> * Add a cleanup patch to drop the unnecessary extern keyword for all
>   function declarations in moduleparam.h.
> 
> [...]

Applied to modules-next, thanks!

[1/3] module: Fix freeing of charp module parameters when CONFIG_SYSFS=n
      commit: deffe1edba626d474fef38007c03646ca5876a0e
[2/3] module: Clean up parse_args() arguments
      commit: 65f535501e2a3378629b8650eca553920de5e5a2
[3/3] module: Remove extern keyword from param prototypes
      commit: 44a063c00fb13cf1f2e8a53a2ab10b232a44954b

Best regards,

	Sami



^ permalink raw reply

* Re: [PATCH v2] module.lds,codetag: force 0 sh_addr for sections
From: Sami Tolvanen @ 2026-03-20 17:45 UTC (permalink / raw)
  To: linux-modules, linux-kernel, Joe Lawrence
  Cc: Luis Chamberlain, Petr Pavlu, Daniel Gomez, Aaron Tomlin,
	Petr Mladek, Josh Poimboeuf
In-Reply-To: <20260305015237.299727-1-joe.lawrence@redhat.com>

On Wed, 04 Mar 2026 20:52:37 -0500, Joe Lawrence wrote:
> Commit 1ba9f8979426 ("vmlinux.lds: Unify TEXT_MAIN, DATA_MAIN, and
> related macros") added .text and made .data, .bss, and .rodata sections
> unconditional in the module linker script, but without an explicit
> address like the other sections in the same file.
> 
> When linking modules with ld.bfd -r, sections defined without an address
> inherit the location counter, resulting in non-zero sh_addr values in
> the .ko.  Relocatable objects are expected to have sh_addr=0 for these
> sections and these non-zero addresses confuse elfutils and have been
> reported to cause segmentation faults in SystemTap [1].
> 
> [...]

Applied to modules-next, thanks!

[1/1] module.lds,codetag: force 0 sh_addr for sections
      commit: 4afc71bba8b7d7841681e7647ae02f5079aaf28f

Best regards,

	Sami



^ permalink raw reply

* Re: [PATCH v3 2/4] kallsyms: extend lineinfo to loadable modules
From: Sasha Levin @ 2026-03-19 16:43 UTC (permalink / raw)
  To: Petr Pavlu
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Daniel Gomez, Greg KH, Petr Mladek,
	Steven Rostedt, Kees Cook, Peter Zijlstra, Thorsten Leemhuis,
	Vlastimil Babka, Helge Deller, Randy Dunlap, Laurent Pinchart,
	Vivian Wang, linux-kernel, linux-kbuild, linux-modules, linux-doc
In-Reply-To: <79244e56-b3ea-4986-b4a2-91a78b21bf07@suse.com>

Thanks for the great review!

On Thu, Mar 19, 2026 at 11:37:18AM +0100, Petr Pavlu wrote:
>On 3/12/26 4:06 AM, Sasha Levin wrote:
>> +- **No init text**: For modules, functions in ``.init.text`` are not annotated
>> +  because that memory is freed after module initialization.
>
>A second table .init.mod_lineinfo could be added to provide the
>necessary information for .init sections, which would be dropped along
>with all the other init code+data.

Sure, it's something we can look at it later. For this series I'd like to keep
scope to runtime .text, since init code runs briefly and rarely appears in
stack traces that need debugging. Adding a second section would require module
loader changes for loading and freeing it alongside init memory.

>> --- /dev/null
>> +++ b/include/linux/mod_lineinfo.h
>> @@ -0,0 +1,68 @@
>> +/* SPDX-License-Identifier: GPL-2.0 */
>> +/*
>> + * mod_lineinfo.h - Binary format for per-module source line information
>> + *
>> + * This header defines the layout of the .mod_lineinfo section embedded
>> + * in loadable kernel modules.  It is dual-use: included from both the
>> + * kernel and the userspace gen_lineinfo tool.
>> + *
>> + * Section layout (all values in target-native endianness):
>> + *
>> + *   struct mod_lineinfo_header     (16 bytes)
>> + *   u32 addrs[num_entries]         -- offsets from .text base, sorted
>
>Modules are relocatable objects. The typical way to express a reference
>from one section to data in another section is to use relocations.
>Choosing to use an implicit base and resolved offsets means that the
>code has trouble correctly referencing the .text section and can't
>express line information data for other sections, such as .exit.text.

I agree, which is why I scoped this just to .text :)

My thinking was that using ELF relocations would add significant complexity to
both the build tool and the runtime lookup path, which must remain NMI-safe and
allocation-free.

>> + *   u16 file_ids[num_entries]      -- parallel to addrs
>> + *   <2-byte pad if num_entries is odd>
>> + *   u32 lines[num_entries]         -- parallel to addrs
>> + *   u32 file_offsets[num_files]    -- byte offset into filenames[]
>> + *   char filenames[filenames_size] -- concatenated NUL-terminated strings
>
>Nit: The description could be a bit easier to navigate if the
>mod_lineinfo_header struct was expanded, so it is clear where
>num_entries, num_files and filenames_size come from.

Makes sense

>> + */
>> +#ifndef _LINUX_MOD_LINEINFO_H
>> +#define _LINUX_MOD_LINEINFO_H
>> +
>> +#ifdef __KERNEL__
>> +#include <linux/types.h>
>> +#else
>> +#include <stdint.h>
>> +typedef uint32_t u32;
>> +typedef uint16_t u16;
>> +#endif
>> +
>> +struct mod_lineinfo_header {
>> +	u32 num_entries;
>> +	u32 num_files;
>> +	u32 filenames_size;	/* total bytes of concatenated filenames */
>
>An alternative would be to say that the filenames data extends to the
>end of the section, without requiring an explicit filenames_size.

I'd prefer to keep filenames_size explicit: it allows lineinfo_search() to
validate filename offsets without knowing the section size. This keeps the
search function reusable between vmlinux (where data comes from linker globals
with no "section size") and modules (where it comes from a section). The cost
is 4 bytes per module.

>> +	u32 reserved;		/* padding, must be 0 */
>
>I believe the format should remain internal to the kernel, so there is
>no need for such a reserved member.

The format is indeed internal and we don't generally concern ourselves with
out-of-tree modules. I've originally added it as a minimal safety net: if the
format ever changes and a stale .ko with an old-format .mod_lineinfo gets
loaded, the kernel would silently misparse lineinfo data.

I don't feel too strongly about it either way, but 4 bytes felt a pretty cheap
price to pay for this safety net :)

>> +static inline u32 mod_lineinfo_lines_off(u32 num_entries)
>> +{
>> +	/* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */
>> +	u32 off = mod_lineinfo_file_ids_off(num_entries) +
>> +		  num_entries * sizeof(u16);
>> +	return (off + 3) & ~3u;
>> +}
>> +
>> +static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries)
>> +{
>> +	return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32);
>> +}
>> +
>> +static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_files)
>> +{
>> +	return mod_lineinfo_file_offsets_off(num_entries) +
>> +	       num_files * sizeof(u32);
>> +}
>
>These helpers are used only from kernel/module/kallsyms.c. I assume they
>are present in this header file to stay close to the description of the
>format.
>
>I personally find them quite verbose. The module_lookup_lineinfo()
>function needs an intimate knowledge of the data format anyway. The code
>in module_lookup_lineinfo() could be replaced with just:
>
>	addrs = base + sizeof(struct mod_lineinfo_header);
>	file_ids = addrs + num_entries * sizeof(u32);
>	lines = (file_ids + num_entries * sizeof(u16) + 3) & ~3u;
>	file_offsets = lines + num_entries * sizeof(u32);
>	filenames = file_offsets + num_files * sizeof(u32);

They are very verbose mostly to make it easy for me to understand :)

Note that the next patch which adds compression rewrites these completely.

I kept these here just to make it easier to understand what's happening during
my development work as well as reviews.

>> +
>> +#endif /* _LINUX_MOD_LINEINFO_H */
>> diff --git a/include/linux/module.h b/include/linux/module.h
>> index 14f391b186c6d..d23e0cd9c7210 100644
>> --- a/include/linux/module.h
>> +++ b/include/linux/module.h
>> @@ -508,6 +508,8 @@ struct module {
>>  	void *btf_data;
>>  	void *btf_base_data;
>>  #endif
>> +	void *lineinfo_data;		/* .mod_lineinfo section in MOD_RODATA */
>> +	unsigned int lineinfo_data_size;
>
>The lineinfo-specific members should be enclosed within the `#ifdef
>CONFIG_KALLSYMS_LINEINFO_MODULES`.
>
>This will require module_lookup_lineinfo() to be conditionally compiled
>based on CONFIG_KALLSYMS_LINEINFO_MODULES, with a dummy version provided
>otherwise. Alternatively, accessors to module::lineinfo_data and
>module::lineinfo_data_size that handle CONFIG_KALLSYMS_LINEINFO_MODULES
>could be introduced in include/linux/module.h. For example, see
>module_buildid() or is_livepatch_module.

The struct members were deliberately left without #ifdef guards following Helge
Deller's suggestion in the v1 review[1]. I don't really mind either way, but
I'd prefer to have a consensus before flipping it back and forth.

Helge?

[1] https://lore.kernel.org/all/3ab0cad6-bf55-4ae5-afb7-d9129ac2032e@gmx.de/

>> +	addrs = base + mod_lineinfo_addrs_off();
>> +	file_ids = base + mod_lineinfo_file_ids_off(num_entries);
>> +	lines = base + mod_lineinfo_lines_off(num_entries);
>> +	file_offsets = base + mod_lineinfo_file_offsets_off(num_entries);
>> +	filenames = base + mod_lineinfo_filenames_off(num_entries, num_files);
>> +
>> +	/* Compute offset from module .text base */
>> +	text_base = (unsigned long)mod->mem[MOD_TEXT].base;
>
>The module::mem[] covers module memory regions. One can think of them as
>ELF segments, except they are created dynamically by the module loader.
>The code conflates the .text section and the TEXT segment. I'm not aware
>of any guarantee that the .text section will be always placed as the
>first section in this segment.

You're right that this conflates section and segment. In practice, .text is
always first in MOD_TEXT because __layout_sections() processes SHF_EXECINSTR
sections in ELF order, and .text is conventionally first.  But I agree this
shouldn't be an implicit assumption.

We can add a validation check at module load time to verify the assumption
for now, and address it better when ...

>Relocations can be used to accurately reference the .text section.

... we add full relocation support.

>> +	if (addr < text_base)
>> +		return false;
>> +
>> +	raw_offset = addr - text_base;
>> +	if (raw_offset > UINT_MAX)
>
>The offsets in the addrs array are of the u32 type, so this should be
>strictly speaking checked against U32_MAX.

Right

>> --- a/scripts/Makefile
>> +++ b/scripts/Makefile
>> @@ -5,6 +5,7 @@
>>
>>  hostprogs-always-$(CONFIG_KALLSYMS)			+= kallsyms
>>  hostprogs-always-$(CONFIG_KALLSYMS_LINEINFO)		+= gen_lineinfo
>> +hostprogs-always-$(CONFIG_KALLSYMS_LINEINFO_MODULES)	+= gen_lineinfo
>
>This line is unnecessary because CONFIG_KALLSYMS_LINEINFO_MODULES
>depends on CONFIG_KALLSYMS_LINEINFO.

It is. I mostly left it here for clarity's sake. I'll drop it. 

>> @@ -59,6 +62,9 @@ if_changed_except = $(if $(call newer_prereqs_except,$(2))$(cmd-check),      \
>>  	+$(call if_changed_except,ld_ko_o,$(objtree)/vmlinux)
>>  ifdef CONFIG_DEBUG_INFO_BTF_MODULES
>>  	+$(if $(newer-prereqs),$(call cmd,btf_ko))
>> +endif
>> +ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
>> +	+$(if $(newer-prereqs),$(call cmd,lineinfo_ko))
>
>Should this be 'if_changed_except.. vmlinux'?

Lineinfo generation doesn't depend on vmlinux - it reads DWARF directly from
the .ko file itself. Unlike BTF (which uses vmlinux as a base for
deduplication), there's no vmlinux prerequisite to exclude.

>> @@ -194,9 +200,45 @@ static const char *make_relative(const char *path, const char *comp_dir)
>>  		return p ? p + 1 : path;
>>  	}
>>
>> -	/* Fall back to basename */
>> -	p = strrchr(path, '/');
>> -	return p ? p + 1 : path;
>> +	/*
>> +	 * Relative path — check for duplicated-path quirk from libdw
>> +	 * on ET_REL files (e.g., "a/b.c/a/b.c" → "a/b.c").
>> +	 */
>
>When does this quirk occur? Is it a bug in libdw?

This occurs with elfutils libdw when processing ET_REL .ko files.  libdw
constructs source paths by concatenating DW_AT_comp_dir with DW_AT_name from
the compilation unit. For modules where both are relative paths with the same
prefix, this can produce doubled results like "net/foo/bar.c/net/foo/bar.c". It
appears to be a libdw quirk with ET_REL DWARF handling.

>> +	{
>> +		size_t len = strlen(path);
>> +
>> +		for (p = path; (p = strchr(p, '/')) != NULL; p++) {
>> +			size_t prefix = p - path;
>> +			size_t rest = len - prefix - 1;
>> +
>> +			if (rest == prefix && !memcmp(path, p + 1, prefix))
>> +				return p + 1;
>> +		}
>
>Isn't this loop same as:
>
>size_t mid = len / 2;
>if (path[mid] == '/' && !memcmp(path, path + mid + 1, mid - 1))
>	return path + mid + 1;

I think so! For a true duplication "X/X", the split is always at the exact
midpoint (len/2), so the loop over every '/' is unnecessary. I'll adopt this
approach with the memcmp length as `mid` rather than `mid - 1` to compare the
full second half.

>> +static unsigned int r_type_abs32(unsigned int e_machine)
>> +{
>> +	switch (e_machine) {
>> +	case EM_X86_64:		return 10;	/* R_X86_64_32 */
>> +	case EM_386:		return 1;	/* R_386_32 */
>> +	case EM_AARCH64:	return 258;	/* R_AARCH64_ABS32 */
>> +	case EM_ARM:		return 2;	/* R_ARM_ABS32 */
>> +	case EM_RISCV:		return 1;	/* R_RISCV_32 */
>> +	case EM_S390:		return 4;	/* R_390_32 */
>> +	case EM_MIPS:		return 2;	/* R_MIPS_32 */
>> +	case EM_PPC64:		return 1;	/* R_PPC64_ADDR32 */
>> +	case EM_PPC:		return 1;	/* R_PPC_ADDR32 */
>> +	case EM_LOONGARCH:	return 1;	/* R_LARCH_32 */
>> +	case EM_PARISC:		return 1;	/* R_PARISC_DIR32 */
>> +	default:		return 0;
>
>The source file already includes elf.h from elfutils. Is it necessary to
>hardcode these relocation values here?

Right!

>>  static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
>>  {
>>  	Dwarf_Off off = 0, next_off;
>> @@ -295,6 +490,16 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
>>  			if (addr < text_addr)
>>  				continue;
>>
>> +			/*
>> +			 * In module mode, keep only .text addresses.
>> +			 * In ET_REL .ko files, .init.text/.exit.text may
>> +			 * overlap with .text address ranges, so we must
>> +			 * explicitly check against the .text bounds.
>> +			 */
>
>Nit: The use of "may" in this comment seems strange. It is fully
>expected that .text, .init.text, .exit.text or any other section will
>have their sh_addr set to 0 in relocatable objects and therefore the
>sections have overlapping address ranges.

I guess I wasn't sure if it's really always the case. We can adjust the comment.

>> +			if (module_mode && text_section_end > text_section_start &&
>> +			    (addr < text_section_start || addr >= text_section_end))
>> +				continue;
>> +
>
>The current code is very specific to the .text section. It would be good
>to cover all sections in the future. I think it will naturally require
>use of relocations to refer to individual sections.

Yup, we can definitely give it a go in the future.

-- 
Thanks,
Sasha

^ permalink raw reply

* Re: [PATCH 12/61] quota: Prefer IS_ERR_OR_NULL over manual NULL check
From: Jan Kara @ 2026-03-19 14:13 UTC (permalink / raw)
  To: Philipp Hahn
  Cc: amd-gfx, apparmor, bpf, ceph-devel, cocci, dm-devel, dri-devel,
	gfs2, intel-gfx, intel-wired-lan, iommu, kvm, linux-arm-kernel,
	linux-block, linux-bluetooth, linux-btrfs, linux-cifs, linux-clk,
	linux-erofs, linux-ext4, linux-fsdevel, linux-gpio, linux-hyperv,
	linux-input, linux-kernel, linux-leds, linux-media, linux-mips,
	linux-mm, linux-modules, linux-mtd, linux-nfs, linux-omap,
	linux-phy, linux-pm, linux-rockchip, linux-s390, linux-scsi,
	linux-sctp, linux-security-module, linux-sh, linux-sound,
	linux-stm32, linux-trace-kernel, linux-usb, linux-wireless,
	netdev, ntfs3, samba-technical, sched-ext, target-devel,
	tipc-discussion, v9fs, Jan Kara
In-Reply-To: <20260310-b4-is_err_or_null-v1-12-bd63b656022d@avm.de>

On Tue 10-03-26 12:48:38, Philipp Hahn wrote:
> Prefer using IS_ERR_OR_NULL() over using IS_ERR() and a manual NULL
> check.
> 
> Change generated with coccinelle.
> 
> To: Jan Kara <jack@suse.com>
> Cc: linux-kernel@vger.kernel.org
> Signed-off-by: Philipp Hahn <phahn-oss@avm.de>

Thanks for the patch but frankly I find the original variant clearer wrt
what is going on. So I prefer to keep the code as is.

								Honza

> ---
>  fs/quota/quota.c | 2 +-
>  1 file changed, 1 insertion(+), 1 deletion(-)
> 
> diff --git a/fs/quota/quota.c b/fs/quota/quota.c
> index 33bacd70758007129e0375bab44d7431195ec441..2e09fc247d0cf45b9e83a4f8a0be7ea694c8c2a1 100644
> --- a/fs/quota/quota.c
> +++ b/fs/quota/quota.c
> @@ -965,7 +965,7 @@ SYSCALL_DEFINE4(quotactl, unsigned int, cmd, const char __user *, special,
>  	else
>  		drop_super_exclusive(sb);
>  out:
> -	if (pathp && !IS_ERR(pathp))
> +	if (!IS_ERR_OR_NULL(pathp))
>  		path_put(pathp);
>  	return ret;
>  }
> 
> -- 
> 2.43.0
> 
-- 
Jan Kara <jack@suse.com>
SUSE Labs, CR

^ permalink raw reply

* Re: [PATCH v3 2/4] kallsyms: extend lineinfo to loadable modules
From: Petr Pavlu @ 2026-03-19 10:37 UTC (permalink / raw)
  To: Sasha Levin
  Cc: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
	Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
	James Bottomley, Jonathan Corbet, Nathan Chancellor,
	Nicolas Schier, Daniel Gomez, Greg KH, Petr Mladek,
	Steven Rostedt, Kees Cook, Peter Zijlstra, Thorsten Leemhuis,
	Vlastimil Babka, Helge Deller, Randy Dunlap, Laurent Pinchart,
	Vivian Wang, linux-kernel, linux-kbuild, linux-modules, linux-doc
In-Reply-To: <20260312030649.674699-3-sashal@kernel.org>

On 3/12/26 4:06 AM, Sasha Levin wrote:
> Add CONFIG_KALLSYMS_LINEINFO_MODULES, which extends the
> CONFIG_KALLSYMS_LINEINFO feature to loadable kernel modules.
> 
> At build time, each .ko is post-processed by scripts/gen-mod-lineinfo.sh
> (modeled on gen-btf.sh) which runs scripts/gen_lineinfo --module on the
> .ko, generates a .mod_lineinfo section containing a compact binary table
> of .text-relative offsets, file IDs, line numbers, and filenames, and
> embeds it back into the .ko via objcopy.
> 
> At runtime, module_lookup_lineinfo() performs a binary search on the
> module's .mod_lineinfo section, and __sprint_symbol() calls it for
> addresses that fall within a module.  The lookup is NMI/panic-safe
> (no locks, no allocations) — the data lives in read-only module memory
> and is freed automatically when the module is unloaded.
> 
> The gen_lineinfo tool gains --module mode which:
>  - Uses .text section address as base (ET_REL files have no _text symbol)
>  - Filters entries to .text-only (excludes .init.text/.exit.text)
>  - Handles libdw's ET_REL path-doubling quirk in make_relative()
>  - Outputs a flat binary-format section instead of named global symbols
> 
> Per-module overhead is approximately 10 bytes per DWARF line entry.
> 
> Assisted-by: Claude:claude-opus-4-6
> Signed-off-by: Sasha Levin <sashal@kernel.org>
> ---
>  .../admin-guide/kallsyms-lineinfo.rst         |  40 +-
>  MAINTAINERS                                   |   2 +
>  include/linux/mod_lineinfo.h                  |  68 ++++
>  include/linux/module.h                        |   5 +
>  init/Kconfig                                  |  13 +
>  kernel/kallsyms.c                             |  18 +-
>  kernel/module/kallsyms.c                      |  91 +++++
>  kernel/module/main.c                          |   3 +
>  scripts/Makefile                              |   1 +
>  scripts/Makefile.modfinal                     |   6 +
>  scripts/gen-mod-lineinfo.sh                   |  48 +++
>  scripts/gen_lineinfo.c                        | 349 ++++++++++++++++--
>  12 files changed, 604 insertions(+), 40 deletions(-)
>  create mode 100644 include/linux/mod_lineinfo.h
>  create mode 100755 scripts/gen-mod-lineinfo.sh
> 
> diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
> index c8ec124394354..5cae995eb118e 100644
> --- a/Documentation/admin-guide/kallsyms-lineinfo.rst
> +++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
> @@ -51,22 +51,46 @@ With ``CONFIG_KALLSYMS_LINEINFO``::
>  Note that assembly routines (such as ``entry_SYSCALL_64_after_hwframe``) are
>  not annotated because they lack DWARF debug information.
>  
> +Module Support
> +==============
> +
> +``CONFIG_KALLSYMS_LINEINFO_MODULES`` extends the feature to loadable kernel
> +modules.  When enabled, each ``.ko`` is post-processed at build time to embed
> +a ``.mod_lineinfo`` section containing the same kind of address-to-source
> +mapping.
> +
> +Enable in addition to the base options::
> +
> +    CONFIG_MODULES=y
> +    CONFIG_KALLSYMS_LINEINFO_MODULES=y
> +
> +Stack traces from module code will then include annotations::
> +
> +    my_driver_func+0x30/0x100 [my_driver] (drivers/foo/bar.c:123)
> +
> +The ``.mod_lineinfo`` section is loaded into read-only module memory alongside
> +the module text.  No additional runtime memory allocation is required; the data
> +is freed when the module is unloaded.
> +
>  Memory Overhead
>  ===============
>  
> -The lineinfo tables are stored in ``.rodata`` and typically add approximately
> -44 MiB to the kernel image for a standard configuration (~4.6 million DWARF
> -line entries, ~10 bytes per entry after deduplication).
> +The vmlinux lineinfo tables are stored in ``.rodata`` and typically add
> +approximately 44 MiB to the kernel image for a standard configuration
> +(~4.6 million DWARF line entries, ~10 bytes per entry after deduplication).
> +
> +Per-module lineinfo adds approximately 10 bytes per DWARF line entry to each
> +``.ko`` file.
>  
>  Known Limitations
>  =================
>  
> -- **vmlinux only**: Only symbols in the core kernel image are annotated.
> -  Module symbols are not covered.
> -- **4 GiB offset limit**: Address offsets from ``_text`` are stored as 32-bit
> -  values.  Entries beyond 4 GiB from ``_text`` are skipped at build time with
> -  a warning.
> +- **4 GiB offset limit**: Address offsets from ``_text`` (vmlinux) or
> +  ``.text`` base (modules) are stored as 32-bit values.  Entries beyond
> +  4 GiB are skipped at build time with a warning.
>  - **65535 file limit**: Source file IDs are stored as 16-bit values.  Builds
>    with more than 65535 unique source files will fail with an error.
>  - **No assembly annotations**: Functions implemented in assembly that lack
>    DWARF ``.debug_line`` data are not annotated.
> +- **No init text**: For modules, functions in ``.init.text`` are not annotated
> +  because that memory is freed after module initialization.

A second table .init.mod_lineinfo could be added to provide the
necessary information for .init sections, which would be dropped along
with all the other init code+data.

> diff --git a/MAINTAINERS b/MAINTAINERS
> index f061e69b6e32a..535e992ca5a20 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -13732,6 +13732,8 @@ KALLSYMS LINEINFO
>  M:	Sasha Levin <sashal@kernel.org>
>  S:	Maintained
>  F:	Documentation/admin-guide/kallsyms-lineinfo.rst
> +F:	include/linux/mod_lineinfo.h
> +F:	scripts/gen-mod-lineinfo.sh
>  F:	scripts/gen_lineinfo.c
>  
>  KASAN
> diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
> new file mode 100644
> index 0000000000000..d62e9608f0f82
> --- /dev/null
> +++ b/include/linux/mod_lineinfo.h
> @@ -0,0 +1,68 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + * mod_lineinfo.h - Binary format for per-module source line information
> + *
> + * This header defines the layout of the .mod_lineinfo section embedded
> + * in loadable kernel modules.  It is dual-use: included from both the
> + * kernel and the userspace gen_lineinfo tool.
> + *
> + * Section layout (all values in target-native endianness):
> + *
> + *   struct mod_lineinfo_header     (16 bytes)
> + *   u32 addrs[num_entries]         -- offsets from .text base, sorted

Modules are relocatable objects. The typical way to express a reference
from one section to data in another section is to use relocations.
Choosing to use an implicit base and resolved offsets means that the
code has trouble correctly referencing the .text section and can't
express line information data for other sections, such as .exit.text.

> + *   u16 file_ids[num_entries]      -- parallel to addrs
> + *   <2-byte pad if num_entries is odd>
> + *   u32 lines[num_entries]         -- parallel to addrs
> + *   u32 file_offsets[num_files]    -- byte offset into filenames[]
> + *   char filenames[filenames_size] -- concatenated NUL-terminated strings

Nit: The description could be a bit easier to navigate if the
mod_lineinfo_header struct was expanded, so it is clear where
num_entries, num_files and filenames_size come from.

> + */
> +#ifndef _LINUX_MOD_LINEINFO_H
> +#define _LINUX_MOD_LINEINFO_H
> +
> +#ifdef __KERNEL__
> +#include <linux/types.h>
> +#else
> +#include <stdint.h>
> +typedef uint32_t u32;
> +typedef uint16_t u16;
> +#endif
> +
> +struct mod_lineinfo_header {
> +	u32 num_entries;
> +	u32 num_files;
> +	u32 filenames_size;	/* total bytes of concatenated filenames */

An alternative would be to say that the filenames data extends to the
end of the section, without requiring an explicit filenames_size.

> +	u32 reserved;		/* padding, must be 0 */

I believe the format should remain internal to the kernel, so there is
no need for such a reserved member.

> +};
> +
> +/* Offset helpers: compute byte offset from start of section to each array */
> +
> +static inline u32 mod_lineinfo_addrs_off(void)
> +{
> +	return sizeof(struct mod_lineinfo_header);
> +}
> +
> +static inline u32 mod_lineinfo_file_ids_off(u32 num_entries)
> +{
> +	return mod_lineinfo_addrs_off() + num_entries * sizeof(u32);
> +}
> +
> +static inline u32 mod_lineinfo_lines_off(u32 num_entries)
> +{
> +	/* u16 file_ids[] may need 2-byte padding to align lines[] to 4 bytes */
> +	u32 off = mod_lineinfo_file_ids_off(num_entries) +
> +		  num_entries * sizeof(u16);
> +	return (off + 3) & ~3u;
> +}
> +
> +static inline u32 mod_lineinfo_file_offsets_off(u32 num_entries)
> +{
> +	return mod_lineinfo_lines_off(num_entries) + num_entries * sizeof(u32);
> +}
> +
> +static inline u32 mod_lineinfo_filenames_off(u32 num_entries, u32 num_files)
> +{
> +	return mod_lineinfo_file_offsets_off(num_entries) +
> +	       num_files * sizeof(u32);
> +}

These helpers are used only from kernel/module/kallsyms.c. I assume they
are present in this header file to stay close to the description of the
format.

I personally find them quite verbose. The module_lookup_lineinfo()
function needs an intimate knowledge of the data format anyway. The code
in module_lookup_lineinfo() could be replaced with just:

	addrs = base + sizeof(struct mod_lineinfo_header);
	file_ids = addrs + num_entries * sizeof(u32);
	lines = (file_ids + num_entries * sizeof(u16) + 3) & ~3u;
	file_offsets = lines + num_entries * sizeof(u32);
	filenames = file_offsets + num_files * sizeof(u32);

> +
> +#endif /* _LINUX_MOD_LINEINFO_H */
> diff --git a/include/linux/module.h b/include/linux/module.h
> index 14f391b186c6d..d23e0cd9c7210 100644
> --- a/include/linux/module.h
> +++ b/include/linux/module.h
> @@ -508,6 +508,8 @@ struct module {
>  	void *btf_data;
>  	void *btf_base_data;
>  #endif
> +	void *lineinfo_data;		/* .mod_lineinfo section in MOD_RODATA */
> +	unsigned int lineinfo_data_size;

The lineinfo-specific members should be enclosed within the `#ifdef
CONFIG_KALLSYMS_LINEINFO_MODULES`.

This will require module_lookup_lineinfo() to be conditionally compiled
based on CONFIG_KALLSYMS_LINEINFO_MODULES, with a dummy version provided
otherwise. Alternatively, accessors to module::lineinfo_data and
module::lineinfo_data_size that handle CONFIG_KALLSYMS_LINEINFO_MODULES
could be introduced in include/linux/module.h. For example, see
module_buildid() or is_livepatch_module.

>  #ifdef CONFIG_JUMP_LABEL
>  	struct jump_entry *jump_entries;
>  	unsigned int num_jump_entries;
> @@ -1021,6 +1023,9 @@ static inline unsigned long find_kallsyms_symbol_value(struct module *mod,
>  
>  #endif  /* CONFIG_MODULES && CONFIG_KALLSYMS */
>  
> +bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
> +			    const char **file, unsigned int *line);
> +
>  /* Define __free(module_put) macro for struct module *. */
>  DEFINE_FREE(module_put, struct module *, if (_T) module_put(_T))
>  
> diff --git a/init/Kconfig b/init/Kconfig
> index c39f27e6393a8..bf53275bc405a 100644
> --- a/init/Kconfig
> +++ b/init/Kconfig
> @@ -2070,6 +2070,19 @@ config KALLSYMS_LINEINFO
>  
>  	  If unsure, say N.
>  
> +config KALLSYMS_LINEINFO_MODULES
> +	bool "Embed source file:line information in module stack traces"
> +	depends on KALLSYMS_LINEINFO && MODULES
> +	help
> +	  Extends KALLSYMS_LINEINFO to loadable kernel modules.  Each .ko
> +	  gets a lineinfo table generated from its DWARF data at build time,
> +	  so stack traces from module code include (file.c:123) annotations.
> +
> +	  Requires elfutils (libdw-dev/elfutils-devel) on the build host.
> +	  Increases .ko sizes by approximately 10 bytes per DWARF line entry.
> +
> +	  If unsure, say N.
> +
>  # end of the "standard kernel features (expert users)" menu
>  
>  config ARCH_HAS_MEMBARRIER_CALLBACKS
> diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
> index d0a9cd9c6dace..9df92b0fd9041 100644
> --- a/kernel/kallsyms.c
> +++ b/kernel/kallsyms.c
> @@ -543,12 +543,24 @@ static int __sprint_symbol(char *buffer, unsigned long address,
>  		len += sprintf(buffer + len, "]");
>  	}
>  
> -	if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) && !modname) {
> +	if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO)) {
>  		const char *li_file;
>  		unsigned int li_line;
> +		bool found = false;
> +
> +		if (!modname)
> +			found = kallsyms_lookup_lineinfo(address,
> +							 &li_file, &li_line);
> +		else if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES)) {
> +			struct module *mod = __module_address(address);
> +
> +			if (mod)
> +				found = module_lookup_lineinfo(mod, address,
> +							      &li_file,
> +							      &li_line);
> +		}
>  
> -		if (kallsyms_lookup_lineinfo(address,
> -					     &li_file, &li_line))
> +		if (found)
>  			len += snprintf(buffer + len, KSYM_SYMBOL_LEN - len,
>  					" (%s:%u)", li_file, li_line);
>  	}
> diff --git a/kernel/module/kallsyms.c b/kernel/module/kallsyms.c
> index 0fc11e45df9b9..5b46293e957ab 100644
> --- a/kernel/module/kallsyms.c
> +++ b/kernel/module/kallsyms.c
> @@ -494,3 +494,94 @@ int module_kallsyms_on_each_symbol(const char *modname,
>  	mutex_unlock(&module_mutex);
>  	return ret;
>  }
> +
> +#include <linux/mod_lineinfo.h>
> +
> +/*
> + * Look up source file:line for an address within a loaded module.
> + * Uses the .mod_lineinfo section embedded in the .ko at build time.
> + *
> + * Safe in NMI/panic context: no locks, no allocations.
> + * Caller must hold RCU read lock (or be in a context where the module
> + * cannot be unloaded).
> + */
> +bool module_lookup_lineinfo(struct module *mod, unsigned long addr,
> +			    const char **file, unsigned int *line)
> +{
> +	const struct mod_lineinfo_header *hdr;
> +	const void *base;
> +	const u32 *addrs, *lines, *file_offsets;
> +	const u16 *file_ids;
> +	const char *filenames;
> +	u32 num_entries, num_files, filenames_size;
> +	unsigned long text_base;
> +	unsigned int offset;
> +	unsigned long long raw_offset;
> +	unsigned int low, high, mid;
> +	u16 file_id;
> +
> +	if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES))
> +		return false;
> +
> +	base = mod->lineinfo_data;
> +	if (!base)
> +		return false;
> +
> +	if (mod->lineinfo_data_size < sizeof(*hdr))
> +		return false;
> +
> +	hdr = base;
> +	num_entries = hdr->num_entries;
> +	num_files = hdr->num_files;
> +	filenames_size = hdr->filenames_size;
> +
> +	if (num_entries == 0)
> +		return false;
> +
> +	/* Validate section is large enough for all arrays */
> +	if (mod->lineinfo_data_size <
> +	    mod_lineinfo_filenames_off(num_entries, num_files) + filenames_size)
> +		return false;
> +
> +	addrs = base + mod_lineinfo_addrs_off();
> +	file_ids = base + mod_lineinfo_file_ids_off(num_entries);
> +	lines = base + mod_lineinfo_lines_off(num_entries);
> +	file_offsets = base + mod_lineinfo_file_offsets_off(num_entries);
> +	filenames = base + mod_lineinfo_filenames_off(num_entries, num_files);
> +
> +	/* Compute offset from module .text base */
> +	text_base = (unsigned long)mod->mem[MOD_TEXT].base;

The module::mem[] covers module memory regions. One can think of them as
ELF segments, except they are created dynamically by the module loader.
The code conflates the .text section and the TEXT segment. I'm not aware
of any guarantee that the .text section will be always placed as the
first section in this segment.

Relocations can be used to accurately reference the .text section.

> +	if (addr < text_base)
> +		return false;
> +
> +	raw_offset = addr - text_base;
> +	if (raw_offset > UINT_MAX)

The offsets in the addrs array are of the u32 type, so this should be
strictly speaking checked against U32_MAX.

> +		return false;
> +	offset = (unsigned int)raw_offset;
> +
> +	/* Binary search for largest entry <= offset */
> +	low = 0;
> +	high = num_entries;
> +	while (low < high) {
> +		mid = low + (high - low) / 2;
> +		if (addrs[mid] <= offset)
> +			low = mid + 1;
> +		else
> +			high = mid;
> +	}
> +
> +	if (low == 0)
> +		return false;
> +	low--;
> +
> +	file_id = file_ids[low];
> +	if (file_id >= num_files)
> +		return false;
> +
> +	if (file_offsets[file_id] >= filenames_size)
> +		return false;
> +
> +	*file = &filenames[file_offsets[file_id]];
> +	*line = lines[low];
> +	return true;
> +}
> diff --git a/kernel/module/main.c b/kernel/module/main.c
> index 2bac4c7cd019a..d11646b02730a 100644
> --- a/kernel/module/main.c
> +++ b/kernel/module/main.c
> @@ -2648,6 +2648,9 @@ static int find_module_sections(struct module *mod, struct load_info *info)
>  	mod->btf_base_data = any_section_objs(info, ".BTF.base", 1,
>  					      &mod->btf_base_data_size);
>  #endif
> +	if (IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES))
> +		mod->lineinfo_data = any_section_objs(info, ".mod_lineinfo", 1,
> +						      &mod->lineinfo_data_size);
>  #ifdef CONFIG_JUMP_LABEL
>  	mod->jump_entries = section_objs(info, "__jump_table",
>  					sizeof(*mod->jump_entries),
> diff --git a/scripts/Makefile b/scripts/Makefile
> index ffe89875b3295..651df2a867ffb 100644
> --- a/scripts/Makefile
> +++ b/scripts/Makefile
> @@ -5,6 +5,7 @@
>  
>  hostprogs-always-$(CONFIG_KALLSYMS)			+= kallsyms
>  hostprogs-always-$(CONFIG_KALLSYMS_LINEINFO)		+= gen_lineinfo
> +hostprogs-always-$(CONFIG_KALLSYMS_LINEINFO_MODULES)	+= gen_lineinfo

This line is unnecessary because CONFIG_KALLSYMS_LINEINFO_MODULES
depends on CONFIG_KALLSYMS_LINEINFO.

>  hostprogs-always-$(BUILD_C_RECORDMCOUNT)		+= recordmcount
>  hostprogs-always-$(CONFIG_BUILDTIME_TABLE_SORT)		+= sorttable
>  hostprogs-always-$(CONFIG_ASN1)				+= asn1_compiler
> diff --git a/scripts/Makefile.modfinal b/scripts/Makefile.modfinal
> index adcbcde16a071..3941cf624526b 100644
> --- a/scripts/Makefile.modfinal
> +++ b/scripts/Makefile.modfinal
> @@ -46,6 +46,9 @@ quiet_cmd_btf_ko = BTF [M] $@
>  		$(CONFIG_SHELL) $(srctree)/scripts/gen-btf.sh --btf_base $(objtree)/vmlinux $@; \
>  	fi;
>  
> +quiet_cmd_lineinfo_ko = LINEINFO [M] $@
> +      cmd_lineinfo_ko = $(CONFIG_SHELL) $(srctree)/scripts/gen-mod-lineinfo.sh $@
> +
>  # Same as newer-prereqs, but allows to exclude specified extra dependencies
>  newer_prereqs_except = $(filter-out $(PHONY) $(1),$?)
>  
> @@ -59,6 +62,9 @@ if_changed_except = $(if $(call newer_prereqs_except,$(2))$(cmd-check),      \
>  	+$(call if_changed_except,ld_ko_o,$(objtree)/vmlinux)
>  ifdef CONFIG_DEBUG_INFO_BTF_MODULES
>  	+$(if $(newer-prereqs),$(call cmd,btf_ko))
> +endif
> +ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
> +	+$(if $(newer-prereqs),$(call cmd,lineinfo_ko))

Should this be 'if_changed_except.. vmlinux'?

>  endif
>  	+$(call cmd,check_tracepoint)
>  
> diff --git a/scripts/gen-mod-lineinfo.sh b/scripts/gen-mod-lineinfo.sh
> new file mode 100755
> index 0000000000000..d0663b862d31b
> --- /dev/null
> +++ b/scripts/gen-mod-lineinfo.sh
> @@ -0,0 +1,48 @@
> +#!/bin/sh
> +# SPDX-License-Identifier: GPL-2.0
> +#
> +# gen-mod-lineinfo.sh - Embed source line info into a kernel module (.ko)
> +#
> +# Reads DWARF from the .ko, generates a .mod_lineinfo section, and
> +# embeds it back into the .ko.  Modeled on scripts/gen-btf.sh.
> +
> +set -e
> +
> +if [ $# -ne 1 ]; then
> +	echo "Usage: $0 <module.ko>" >&2
> +	exit 1
> +fi
> +
> +KO="$1"
> +
> +cleanup() {
> +	rm -f "${KO}.lineinfo.S" "${KO}.lineinfo.o" "${KO}.lineinfo.bin"
> +}
> +trap cleanup EXIT
> +
> +case "${KBUILD_VERBOSE}" in
> +*1*)
> +	set -x
> +	;;
> +esac
> +
> +# Generate assembly from DWARF -- if it fails (no DWARF), silently skip
> +if ! ${objtree}/scripts/gen_lineinfo --module "${KO}" > "${KO}.lineinfo.S"; then
> +	exit 0
> +fi
> +
> +# Compile assembly to object file
> +${CC} ${NOSTDINC_FLAGS} ${LINUXINCLUDE} ${KBUILD_CPPFLAGS} \
> +	${KBUILD_AFLAGS} ${KBUILD_AFLAGS_MODULE} \
> +	-c -o "${KO}.lineinfo.o" "${KO}.lineinfo.S"
> +
> +# Extract raw section content
> +${OBJCOPY} -O binary --only-section=.mod_lineinfo \
> +	"${KO}.lineinfo.o" "${KO}.lineinfo.bin"
> +
> +# Embed into the .ko with alloc,readonly flags
> +${OBJCOPY} --add-section ".mod_lineinfo=${KO}.lineinfo.bin" \
> +	--set-section-flags .mod_lineinfo=alloc,readonly \
> +	"${KO}"
> +
> +exit 0
> diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
> index 37d5e84971be4..5ced6897cbbee 100644
> --- a/scripts/gen_lineinfo.c
> +++ b/scripts/gen_lineinfo.c
> @@ -23,8 +23,16 @@
>  #include <gelf.h>
>  #include <limits.h>
>  
> +#include "../include/linux/mod_lineinfo.h"
> +
> +static int module_mode;
> +
>  static unsigned int skipped_overflow;
>  
> +/* .text range for module mode (keep only runtime code) */
> +static unsigned long long text_section_start;
> +static unsigned long long text_section_end;
> +
>  struct line_entry {
>  	unsigned int offset;	/* offset from _text */
>  	unsigned int file_id;
> @@ -148,27 +156,25 @@ static const char *make_relative(const char *path, const char *comp_dir)
>  {
>  	const char *p;
>  
> -	/* If already relative, use as-is */
> -	if (path[0] != '/')
> -		return path;
> -
> -	/* comp_dir from DWARF is the most reliable method */
> -	if (comp_dir) {
> -		size_t len = strlen(comp_dir);
> -
> -		if (!strncmp(path, comp_dir, len) && path[len] == '/') {
> -			const char *rel = path + len + 1;
> -
> -			/*
> -			 * If comp_dir pointed to a subdirectory
> -			 * (e.g. arch/parisc/kernel) rather than
> -			 * the tree root, stripping it leaves a
> -			 * bare filename.  Fall through to the
> -			 * kernel_dirs scan so we recover the full
> -			 * relative path instead.
> -			 */
> -			if (strchr(rel, '/'))
> -				return rel;
> +	if (path[0] == '/') {
> +		/* Try comp_dir prefix from DWARF */
> +		if (comp_dir) {
> +			size_t len = strlen(comp_dir);
> +
> +			if (!strncmp(path, comp_dir, len) && path[len] == '/') {
> +				const char *rel = path + len + 1;
> +
> +				/*
> +				 * If comp_dir pointed to a subdirectory
> +				 * (e.g. arch/parisc/kernel) rather than
> +				 * the tree root, stripping it leaves a
> +				 * bare filename.  Fall through to the
> +				 * kernel_dirs scan so we recover the full
> +				 * relative path instead.
> +				 */
> +				if (strchr(rel, '/'))
> +					return rel;
> +			}
>  		}
>  
>  		/*
> @@ -194,9 +200,45 @@ static const char *make_relative(const char *path, const char *comp_dir)
>  		return p ? p + 1 : path;
>  	}
>  
> -	/* Fall back to basename */
> -	p = strrchr(path, '/');
> -	return p ? p + 1 : path;
> +	/*
> +	 * Relative path — check for duplicated-path quirk from libdw
> +	 * on ET_REL files (e.g., "a/b.c/a/b.c" → "a/b.c").
> +	 */

When does this quirk occur? Is it a bug in libdw?

> +	{
> +		size_t len = strlen(path);
> +
> +		for (p = path; (p = strchr(p, '/')) != NULL; p++) {
> +			size_t prefix = p - path;
> +			size_t rest = len - prefix - 1;
> +
> +			if (rest == prefix && !memcmp(path, p + 1, prefix))
> +				return p + 1;
> +		}

Isn't this loop same as:

size_t mid = len / 2;
if (path[mid] == '/' && !memcmp(path, path + mid + 1, mid - 1))
	return path + mid + 1;

> +	}
> +
> +	/*
> +	 * Bare filename with no directory component — try to recover the
> +	 * relative path using comp_dir.  Some toolchains/elfutils combos
> +	 * produce bare filenames where comp_dir holds the source directory.
> +	 * Construct the absolute path and run the kernel_dirs scan.
> +	 */
> +	if (!strchr(path, '/') && comp_dir && comp_dir[0] == '/') {
> +		static char buf[PATH_MAX];
> +
> +		snprintf(buf, sizeof(buf), "%s/%s", comp_dir, path);
> +		for (p = buf + 1; *p; p++) {
> +			if (*(p - 1) == '/') {
> +				for (unsigned int i = 0; i < sizeof(kernel_dirs) /
> +				     sizeof(kernel_dirs[0]); i++) {
> +					if (!strncmp(p, kernel_dirs[i],
> +						     strlen(kernel_dirs[i])))
> +						return p;
> +				}
> +			}
> +		}
> +	}
> +
> +	return path;
>  }
>  
>  static int compare_entries(const void *a, const void *b)
> @@ -248,6 +290,159 @@ static unsigned long long find_text_addr(Elf *elf)
>  	exit(1);
>  }
>  
> +static void find_text_section_range(Elf *elf)
> +{
> +	Elf_Scn *scn = NULL;
> +	GElf_Shdr shdr;
> +	size_t shstrndx;
> +
> +	if (elf_getshdrstrndx(elf, &shstrndx) != 0)
> +		return;
> +
> +	while ((scn = elf_nextscn(elf, scn)) != NULL) {
> +		const char *name;
> +
> +		if (!gelf_getshdr(scn, &shdr))
> +			continue;
> +		name = elf_strptr(elf, shstrndx, shdr.sh_name);
> +		if (name && !strcmp(name, ".text")) {
> +			text_section_start = shdr.sh_addr;
> +			text_section_end = shdr.sh_addr + shdr.sh_size;
> +			return;
> +		}
> +	}
> +}
> +
> +/*
> + * Apply .rela.debug_line relocations to a mutable copy of .debug_line data.
> + *
> + * elfutils libdw (through at least 0.194) does NOT apply relocations for
> + * ET_REL files when using dwarf_begin_elf().  The internal libdwfl layer
> + * does this via __libdwfl_relocate(), but that API is not public.
> + *
> + * For DWARF5, the .debug_line file name table uses DW_FORM_line_strp
> + * references into .debug_line_str.  Without relocation, all these offsets
> + * resolve to 0 (or garbage), causing dwarf_linesrc()/dwarf_filesrc() to
> + * return wrong filenames (typically the comp_dir for every file).
> + *
> + * This function applies the relocations manually so that the patched
> + * .debug_line data can be fed to dwarf_begin_elf() and produce correct
> + * results.
> + *
> + * See elfutils bug https://sourceware.org/bugzilla/show_bug.cgi?id=31447
> + * A fix (dwelf_elf_apply_relocs) was proposed but not yet merged as of
> + * elfutils 0.194: https://sourceware.org/pipermail/elfutils-devel/2024q3/007388.html
> + */
> +/*
> + * Determine the relocation type for a 32-bit absolute reference
> + * on the given architecture.  Returns 0 if unknown.
> + */
> +static unsigned int r_type_abs32(unsigned int e_machine)
> +{
> +	switch (e_machine) {
> +	case EM_X86_64:		return 10;	/* R_X86_64_32 */
> +	case EM_386:		return 1;	/* R_386_32 */
> +	case EM_AARCH64:	return 258;	/* R_AARCH64_ABS32 */
> +	case EM_ARM:		return 2;	/* R_ARM_ABS32 */
> +	case EM_RISCV:		return 1;	/* R_RISCV_32 */
> +	case EM_S390:		return 4;	/* R_390_32 */
> +	case EM_MIPS:		return 2;	/* R_MIPS_32 */
> +	case EM_PPC64:		return 1;	/* R_PPC64_ADDR32 */
> +	case EM_PPC:		return 1;	/* R_PPC_ADDR32 */
> +	case EM_LOONGARCH:	return 1;	/* R_LARCH_32 */
> +	case EM_PARISC:		return 1;	/* R_PARISC_DIR32 */
> +	default:		return 0;

The source file already includes elf.h from elfutils. Is it necessary to
hardcode these relocation values here?

> +	}
> +}
> +
> +static void apply_debug_line_relocations(Elf *elf)
> +{
> +	Elf_Scn *scn = NULL;
> +	Elf_Scn *debug_line_scn = NULL;
> +	Elf_Scn *rela_debug_line_scn = NULL;
> +	Elf_Scn *symtab_scn = NULL;
> +	GElf_Shdr shdr;
> +	GElf_Ehdr ehdr;
> +	unsigned int abs32_type;
> +	size_t shstrndx;
> +	Elf_Data *dl_data, *rela_data, *sym_data;
> +	GElf_Shdr rela_shdr, sym_shdr;
> +	size_t nrels, i;
> +
> +	if (gelf_getehdr(elf, &ehdr) == NULL)
> +		return;
> +
> +	abs32_type = r_type_abs32(ehdr.e_machine);
> +	if (!abs32_type)
> +		return;
> +
> +	if (elf_getshdrstrndx(elf, &shstrndx) != 0)
> +		return;
> +
> +	/* Find the relevant sections */
> +	while ((scn = elf_nextscn(elf, scn)) != NULL) {
> +		const char *name;
> +
> +		if (!gelf_getshdr(scn, &shdr))
> +			continue;
> +		name = elf_strptr(elf, shstrndx, shdr.sh_name);
> +		if (!name)
> +			continue;
> +
> +		if (!strcmp(name, ".debug_line"))
> +			debug_line_scn = scn;
> +		else if (!strcmp(name, ".rela.debug_line"))
> +			rela_debug_line_scn = scn;
> +		else if (shdr.sh_type == SHT_SYMTAB)
> +			symtab_scn = scn;
> +	}
> +
> +	if (!debug_line_scn || !rela_debug_line_scn || !symtab_scn)
> +		return;
> +
> +	dl_data = elf_getdata(debug_line_scn, NULL);
> +	rela_data = elf_getdata(rela_debug_line_scn, NULL);
> +	sym_data = elf_getdata(symtab_scn, NULL);
> +	if (!dl_data || !rela_data || !sym_data)
> +		return;
> +
> +	if (!gelf_getshdr(rela_debug_line_scn, &rela_shdr))
> +		return;
> +	if (!gelf_getshdr(symtab_scn, &sym_shdr))
> +		return;
> +
> +	nrels = rela_shdr.sh_size / rela_shdr.sh_entsize;
> +
> +	for (i = 0; i < nrels; i++) {
> +		GElf_Rela rela;
> +		GElf_Sym sym;
> +		unsigned int r_type;
> +		size_t r_sym;
> +		uint32_t value;
> +
> +		if (!gelf_getrela(rela_data, i, &rela))
> +			continue;
> +
> +		r_type = GELF_R_TYPE(rela.r_info);
> +		r_sym = GELF_R_SYM(rela.r_info);
> +
> +		/* Only handle the 32-bit absolute reloc for this arch */
> +		if (r_type != abs32_type)
> +			continue;
> +
> +		if (!gelf_getsym(sym_data, r_sym, &sym))
> +			continue;
> +
> +		/* Relocated value = sym.st_value + addend */
> +		value = (uint32_t)(sym.st_value + rela.r_addend);
> +
> +		/* Patch the .debug_line data at the relocation offset */
> +		if (rela.r_offset + 4 <= dl_data->d_size)
> +			memcpy((char *)dl_data->d_buf + rela.r_offset,
> +			       &value, sizeof(value));
> +	}
> +}
> +
>  static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
>  {
>  	Dwarf_Off off = 0, next_off;
> @@ -295,6 +490,16 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
>  			if (addr < text_addr)
>  				continue;
>  
> +			/*
> +			 * In module mode, keep only .text addresses.
> +			 * In ET_REL .ko files, .init.text/.exit.text may
> +			 * overlap with .text address ranges, so we must
> +			 * explicitly check against the .text bounds.
> +			 */

Nit: The use of "may" in this comment seems strange. It is fully
expected that .text, .init.text, .exit.text or any other section will
have their sh_addr set to 0 in relocatable objects and therefore the
sections have overlapping address ranges.

> +			if (module_mode && text_section_end > text_section_start &&
> +			    (addr < text_section_start || addr >= text_section_end))
> +				continue;
> +

The current code is very specific to the .text section. It would be good
to cover all sections in the future. I think it will naturally require
use of relocations to refer to individual sections.

>  			{
>  				unsigned long long raw_offset = addr - text_addr;
>  
> @@ -440,6 +645,63 @@ static void output_assembly(void)
>  	printf("\n");
>  }
>  
> +static void output_module_assembly(void)
> +{
> +	unsigned int filenames_size = 0;
> +
> +	for (unsigned int i = 0; i < num_files; i++)
> +		filenames_size += strlen(files[i].name) + 1;
> +
> +	printf("/* SPDX-License-Identifier: GPL-2.0 */\n");
> +	printf("/*\n");
> +	printf(" * Automatically generated by scripts/gen_lineinfo --module\n");
> +	printf(" * Do not edit.\n");
> +	printf(" */\n\n");
> +
> +	printf("\t.section .mod_lineinfo, \"a\"\n\n");
> +
> +	/* Header: num_entries, num_files, filenames_size, reserved */
> +	printf("\t.balign 4\n");
> +	printf("\t.long %u\n", num_entries);
> +	printf("\t.long %u\n", num_files);
> +	printf("\t.long %u\n", filenames_size);
> +	printf("\t.long 0\n\n");
> +
> +	/* addrs[] */
> +	for (unsigned int i = 0; i < num_entries; i++)
> +		printf("\t.long 0x%x\n", entries[i].offset);
> +	if (num_entries)
> +		printf("\n");
> +
> +	/* file_ids[] */
> +	for (unsigned int i = 0; i < num_entries; i++)
> +		printf("\t.short %u\n", entries[i].file_id);
> +
> +	/* Padding to align lines[] to 4 bytes */
> +	if (num_entries & 1)
> +		printf("\t.short 0\n");
> +	if (num_entries)
> +		printf("\n");
> +
> +	/* lines[] */
> +	for (unsigned int i = 0; i < num_entries; i++)
> +		printf("\t.long %u\n", entries[i].line);
> +	if (num_entries)
> +		printf("\n");
> +
> +	/* file_offsets[] */
> +	for (unsigned int i = 0; i < num_files; i++)
> +		printf("\t.long %u\n", files[i].str_offset);
> +	if (num_files)
> +		printf("\n");
> +
> +	/* filenames[] */
> +	for (unsigned int i = 0; i < num_files; i++)
> +		print_escaped_asciz(files[i].name);
> +	if (num_files)
> +		printf("\n");
> +}
> +
>  int main(int argc, char *argv[])
>  {
>  	int fd;
> @@ -447,12 +709,23 @@ int main(int argc, char *argv[])
>  	Dwarf *dwarf;
>  	unsigned long long text_addr;
>  
> +	if (argc >= 2 && !strcmp(argv[1], "--module")) {
> +		module_mode = 1;
> +		argv++;
> +		argc--;
> +	}
> +
>  	if (argc != 2) {
> -		fprintf(stderr, "Usage: %s <vmlinux>\n", argv[0]);
> +		fprintf(stderr, "Usage: %s [--module] <ELF file>\n", argv[0]);
>  		return 1;
>  	}
>  
> -	fd = open(argv[1], O_RDONLY);
> +	/*
> +	 * For module mode, open O_RDWR so we can apply debug section
> +	 * relocations to the in-memory ELF data.  The modifications
> +	 * are NOT written back to disk (no elf_update() call).
> +	 */
> +	fd = open(argv[1], module_mode ? O_RDWR : O_RDONLY);
>  	if (fd < 0) {
>  		fprintf(stderr, "Cannot open %s: %s\n", argv[1],
>  			strerror(errno));
> @@ -460,7 +733,7 @@ int main(int argc, char *argv[])
>  	}
>  
>  	elf_version(EV_CURRENT);
> -	elf = elf_begin(fd, ELF_C_READ, NULL);
> +	elf = elf_begin(fd, module_mode ? ELF_C_RDWR : ELF_C_READ, NULL);
>  	if (!elf) {
>  		fprintf(stderr, "elf_begin failed: %s\n",
>  			elf_errmsg(elf_errno()));
> @@ -468,7 +741,22 @@ int main(int argc, char *argv[])
>  		return 1;
>  	}
>  
> -	text_addr = find_text_addr(elf);
> +	if (module_mode) {
> +		/*
> +		 * .ko files are ET_REL after ld -r.  libdw does NOT apply
> +		 * relocations for ET_REL files, so DW_FORM_line_strp
> +		 * references in .debug_line are not resolved.  Apply them
> +		 * ourselves so that dwarf_linesrc() returns correct paths.
> +		 *
> +		 * DWARF addresses include the .text sh_addr.  Use .text
> +		 * sh_addr as the base so offsets are .text-relative.
> +		 */
> +		apply_debug_line_relocations(elf);
> +		find_text_section_range(elf);
> +		text_addr = text_section_start;
> +	} else {
> +		text_addr = find_text_addr(elf);
> +	}
>  
>  	dwarf = dwarf_begin_elf(elf, DWARF_C_READ, NULL);
>  	if (!dwarf) {
> @@ -494,7 +782,10 @@ int main(int argc, char *argv[])
>  	fprintf(stderr, "lineinfo: %u entries, %u files\n",
>  		num_entries, num_files);
>  
> -	output_assembly();
> +	if (module_mode)
> +		output_module_assembly();
> +	else
> +		output_assembly();
>  
>  	dwarf_end(dwarf);
>  	elf_end(elf);

-- 
Thanks,
Petr

^ permalink raw reply

* Re: [PATCH v2 1/2] kthread: remove kthread_exit()
From: Steven Rostedt @ 2026-03-18 23:12 UTC (permalink / raw)
  To: David Laight
  Cc: Christian Brauner, Linus Torvalds, linux-kernel, linux-modules,
	linux-nfs, bpf, kunit-dev, linux-doc, linux-trace-kernel, netfs,
	io-uring, audit, rcu, kvm, virtualization, netdev, linux-mm,
	linux-security-module, Christian Loehle, linux-fsdevel
In-Reply-To: <20260311104736.51b53405@pumpkin>

On Wed, 11 Mar 2026 10:47:36 +0000
David Laight <david.laight.linux@gmail.com> wrote:

> > -#define module_put_and_kthread_exit(code) kthread_exit(code)
> > +#define module_put_and_kthread_exit(code) do_exit(code)  
> 
> I'm intrigued...
> How does that actually know to do the module_put()?
> (I know it does one - otherwise my driver wouldn't unload.)

It's in the !CONFIG_MODULES section. No module_put() necessary. Only the
kthread_exit (do_exit) is needed.

-- Steve

^ permalink raw reply

* Re: [PATCH v18 00/42] DEPT(DEPendency Tracker)
From: Yunseong Kim @ 2026-03-18  5:58 UTC (permalink / raw)
  To: torvalds, mingo, Byungchul Park, linux-kernel
  Cc: kernel_team, damien.lemoal, linux-ide, adilger.kernel, linux-ext4,
	peterz, will, tglx, rostedt, joel, sashal, daniel.vetter,
	duyuyang, johannes.berg, tj, tytso, willy, david, amir73il,
	gregkh, kernel-team, linux-mm, akpm, mhocko, minchan, hannes,
	vdavydov.dev, sj, jglisse, dennis, cl, penberg, rientjes, vbabka,
	ngupta, linux-block, josef, linux-fsdevel, jack, jlayton,
	dan.j.williams, hch, djwong, dri-devel, rodrigosiqueiramelo,
	melissa.srw, hamohammed.sa, harry.yoo, chris.p.wilson,
	gwan-gyeong.mun, max.byungchul.park, boqun.feng, longman,
	yunseong.kim, yeoreum.yun, netdev, matthew.brost, her0gyugyu,
	corbet, catalin.marinas, bp, x86, hpa, luto, sumit.semwal,
	gustavo, christian.koenig, andi.shyti, arnd, lorenzo.stoakes,
	Liam.Howlett, rppt, surenb, mcgrof, petr.pavlu, da.gomez,
	samitolvanen, paulmck, frederic, neeraj.upadhyay, joelagnelf,
	josh, urezki, mathieu.desnoyers, jiangshanlai, qiang.zhang,
	juri.lelli, vincent.guittot, dietmar.eggemann, bsegall, mgorman,
	vschneid, chuck.lever, neil, okorniev, Dai.Ngo, tom, trondmy,
	anna, kees, bigeasy, clrkwllms, mark.rutland, ada.coupriediaz,
	kristina.martsenko, wangkefeng.wang, broonie, kevin.brodsky, dwmw,
	shakeel.butt, ast, ziy, yuzhao, baolin.wang, usamaarif642,
	joel.granados, richard.weiyang, geert+renesas, tim.c.chen, linux,
	alexander.shishkin, lillian, chenhuacai, francesco,
	guoweikang.kernel, link, jpoimboe, masahiroy, brauner,
	thomas.weissschuh, oleg, mjguzik, andrii, wangfushuai, linux-doc,
	linux-arm-kernel, linux-media, linaro-mm-sig, linux-i2c,
	linux-arch, linux-modules, rcu, linux-nfs, linux-rt-devel,
	2407018371, dakr, miguel.ojeda.sandonis, neilb, bagasdotme,
	wsa+renesas, dave.hansen, geert, ojeda, alex.gaynor, gary,
	bjorn3_gh, lossin, a.hindborg, aliceryhl, tmgross, rust-for-linux
In-Reply-To: <20260317044459.GA27353@system.software.com>

Hi all,

I would like to support this proposal by noting that we are currently tracking
a number of kernel bugs uniquely detectable by DEPT.

In my experience running kernel fuzzers, many of these issues were previously
flagged as hung task and left unresolved because they involved wait/event
dependencies for PG_locked and writeback that are difficult to track otherwise.
DEPT provides the clarity necessary to address these cases. I believe having
DEPT in the mainline tree will be a significant asset for identifying and fixing
these kinds of hidden, latent bugs.

Best regards,
Yunseong


On 3/17/26 1:44 PM, Byungchul Park wrote:
> On Fri, Dec 05, 2025 at 04:18:13PM +0900, Byungchul Park wrote:
>> I'm happy to see that DEPT reported real problems in practice:
>>
>>    https://lore.kernel.org/lkml/6383cde5-cf4b-facf-6e07-1378a485657d@I-love.SAKURA.ne.jp/
>>    https://lore.kernel.org/lkml/1674268856-31807-1-git-send-email-byungchul.park@lge.com/
>>    https://lore.kernel.org/all/b6e00e77-4a8c-4e05-ab79-266bf05fcc2d@igalia.com/
>>
>> I’ve added documentation describing DEPT — this should help you
>> understand what DEPT is and how it works.  You can use DEPT simply by
>> enabling CONFIG_DEPT and checking dmesg at runtime.
>> ---
>>
>> Hi Linus and folks,
>
> Hi Linus and Ingo,
>
> Although the DEPT tool still has areas for improvement, I am confident
> it employs the most appropriate method for tracking dependencies within
> Linux kernel.
>
> If it is to be maintained in the source tree, which subsystem would be
> the most suitable for its management?  Personally, I believe introducing
> a dedicated dependency subsystem under 'kernel/' would be ideal, though
> managing it within the already well-maintained scheduler or locking
> subsystems might be more practical.
>
> Since DEPT tracks not only locking mechanisms but also general sleep and
> wake events, I have avoided placing it under the locking subsystem thus
> far.  If you have alternative or more fitting suggestions, I would
> appreciate your input.
>
> Thanks.
>
>       Byungchul
>>
>> I’ve been developing a tool to detect deadlock possibilities by tracking
>> waits/events — rather than lock acquisition order — to cover all the
>> synchronization mechanisms.  To summarize the design rationale, starting
>> from the problem statement, through analysis, to the solution:
>>
>>    CURRENT STATUS
>>    --------------
>>    Lockdep tracks lock acquisition order to identify deadlock conditions.
>>    Additionally, it tracks IRQ state changes — via {en,dis}able — to
>>    detect cases where locks are acquired unintentionally during
>>    interrupt handling.
>>
>>    PROBLEM
>>    -------
>>    Waits and their associated events that are never reachable can
>>    eventually lead to deadlocks.  However, since Lockdep focuses solely
>>    on lock acquisition order, it has inherent limitations when handling
>>    waits and events.
>>
>>    Moreover, by tracking only lock acquisition order, Lockdep cannot
>>    properly handle read locks or cross-event scenarios — such as
>>    wait_for_completion() and complete() — making it increasingly
>>    inadequate as a general-purpose deadlock detection tool.
>>
>>    SOLUTION
>>    --------
>>    Once again, waits and their associated events that are never
>>    reachable can eventually lead to deadlocks.  The new solution, DEPT,
>>    focuses directly on waits and events.  DEPT monitors waits and events,
>>    and reports them when any become unreachable.
>>
>> DEPT provides:
>>
>>    * Correct handling of read locks.
>>    * Support for general waits and events.
>>    * Continuous operation, even after multiple reports.
>>    * Simple, intuitive annotation APIs.
>>
>> There are still false positives, and some are already being worked on
>> for suppression.  Especially splitting the folio class into several
>> appropriate classes e.g. block device mapping class and regular file
>> mapping class, is currently under active development by me and Yeoreum
>> Yun.
>>
>> Anyway, these efforts will need to continue for a while, as we’ve seen
>> with lockdep over two decades.  DEPT is tagged as EXPERIMENTAL in
>> Kconfig — meaning it’s not yet suitable for use as an automation tool.
>>
>> However, for those who are interested in using DEPT to analyze complex
>> synchronization patterns and extract dependency insights, DEPT would be
>> a great tool for the purpose.
>>
>> Thanks for your support and contributions to:
>>
>>    Harry Yoo <harry.yoo@oracle.com>
>>    Gwan-gyeong Mun <gwan-gyeong.mun@intel.com>
>>    Yunseong Kim <ysk@kzalloc.com>
>>    Yeoreum Yun <yeoreum.yun@arm.com>
>>
>> FAQ
>> ---
>> Q. Is this the first attempt to solve this problem?
>> A. No. The cross-release feature (commit b09be676e0ff2) attempted to
>>    address it — as a Lockdep extension.  It was merged, but quickly
>>    reverted, because:
>>
>>    While it uncovered valuable hidden issues, it also introduced false
>>    positives.  Since these false positives mask further real problems
>>    with Lockdep — and developers strongly dislike them — the feature was
>>    rolled back.
>>
>> Q. Why wasn’t DEPT built as a Lockdep extension?
>> A. Lockdep is the result of years of work by kernel developers — and is
>>    now very stable. But I chose to build DEPT separately, because:
>>
>>    While reusing BFS(Breadth First Search) and Lockdep’s hashing is
>>    beneficial, the rest of the system must be rebuilt from scratch to
>>    align with DEPT’s wait-event model — since Lockdep was originally
>>    designed for tracking lock acquisition orders, not wait-event
>>    dependencies.
>>
>> Q. Do you plan to replace Lockdep entirely?
>> A. Not at all — Lockdep still plays a vital role in validating correct
>>    lock usage.  While its dependency-checking logic should eventually be
>>    superseded by DEPT, the rest of its functionality should stay.
>>
>> Q. Should we replace the dependency check immediately?
>> A. Absolutely not.  Lockdep’s stability is the result of years of hard
>>    work by kernel developers.  Lockdep and DEPT should run side by side
>>    until DEPT matures.
>>
>> Q. Stronger detection often leads to more false positives — which was a
>>    major pain point when cross-release was added.  Is DEPT designed to
>>    handle this?
>> A. Yes.  DEPT’s simple, generalized design enables flexible reporting —
>>    so while false positives still need fixing, they’re far less
>>    disruptive than they were under the Lockdep extension, cross-release.
>>
>> Q. Why not fix all false positives out-of-tree before merging?
>> A. Since the affected subsystems span the entire kernel, like Lockdep,
>>    which has relied on annotations to avoid false positives over the
>>    last two decades, DEPT too will require the annotation efforts.
>>
>>    Performing annotation work within the mainline will help us add
>>    annotations more appropriately and will also make DEPT a useful tool
>>    for a wider range of users more quickly.
>>
>>    CONFIG_DEPT is marked EXPERIMENTAL, so it’s opt-in. Some users are
>>    already interested in using DEPT to analyze complex synchronization
>>    patterns and extract dependency insights.
>>
>>      Byungchul
>> ---
>> Changes from v17:
>>
>>      1. Rebase on the mainline as of 2025 Dec 5.
>>      2. Convert the documents' format from txt to rst. (feedbacked
>>         by Jonathan Corbet and Bagas Sanjaya)
>>      3. Move the documents from 'Documentation/dependency' to
>>         'Documentation/dev-tools'. (feedbakced by Jonathan Corbet)
>>      4. Improve the documentation. (feedbacked by NeilBrown)
>>      5. Use a common function, enter_from_user_mode(), instead of
>>         arch specific code, to notice context switch from user mode.
>>         (feedbacked by Dave Hansen, Mark Rutland, and Mark Brown)
>>      6. Resolve the header dependency issue by using dept's internal
>>         header, instead of relocating 'struct llist_{head,node}' to
>>         another header. (feedbacked by Greg KH)
>>      7. Improve page(or folio) usage type APIs.
>>      8. Add rust helper for wait_for_completion(). (feedbacked by
>>         Guangbo Cui, Boqun Feng, and Danilo Krummrich)
>>      9. Refine some commit messages.
>>
>> Changes from v16:
>>
>>      1. Rebase on v6.17.
>>      2. Fix a false positive from rcu (by Yunseong Kim)
>>      3. Introduce APIs to set page's usage, dept_set_page_usage() and
>>         dept_reset_page_usage() to avoid false positives.
>>      4. Consider lock_page() as a potential wait unconditionally.
>>      5. Consider folio_lock_killable() as a potential wait
>>         unconditionally.
>>      6. Add support for tracking PG_writeback waits and events.
>>      7. Fix two build errors due to the additional debug information
>>         added by dept. (by Yunseong Kim)
>>
>> Changes from v15:
>>
>>      1. Fix typo and improve comments and commit messages (feedbacked
>>         by ALOK TIWARI, Waiman Long, and kernel test robot).
>>      2. Do not stop dept on detection of cicular dependency of
>>         recover event, allowing to keep reporting.
>>      3. Add SK hynix to copyright.
>>      4. Consider folio_lock() as a potential wait unconditionally.
>>      5. Fix Kconfig dependency bug (feedbacked by kernel test rebot).
>>      6. Do not suppress reports that involve classes even that have
>>         already involved in other reports, allowing to keep
>>         reporting.
>>
>> Changes from v14:
>>
>>      1. Rebase on the current latest, v6.15-rc6.
>>      2. Refactor dept code.
>>      3. With multi event sites for a single wait, even if an event
>>         forms a circular dependency, the event can be recovered by
>>         other event(or wake up) paths.  Even though informing the
>>         circular dependency is worthy but it should be suppressed
>>         once informing it, if it doesn't lead an actual deadlock.  So
>>         introduce APIs to annotate the relationship between event
>>         site and recover site, that are, event_site() and
>>         dept_recover_event().
>>      4. wait_for_completion() worked with dept map embedded in struct
>>         completion.  However, it generates a few false positves since
>>         all the waits using the instance of struct completion, share
>>         the map and key.  To avoid the false positves, make it not to
>>         share the map and key but each wait_for_completion() caller
>>         have its own key by default.  Of course, external maps also
>>         can be used if needed.
>>      5. Fix a bug about hardirq on/off tracing.
>>      6. Implement basic unit test for dept.
>>      7. Add more supports for dma fence synchronization.
>>      8. Add emergency stop of dept e.g. on panic().
>>      9. Fix false positives by mmu_notifier_invalidate_*().
>>      10. Fix recursive call bug by DEPT_WARN_*() and DEPT_STOP().
>>      11. Fix trivial bugs in DEPT_WARN_*() and DEPT_STOP().
>>      12. Fix a bug that a spin lock, dept_pool_spin, is used in
>>          both contexts of irq disabled and enabled without irq
>>          disabled.
>>      13. Suppress reports with classes, any of that already have
>>          been reported, even though they have different chains but
>>          being barely meaningful.
>>      14. Print stacktrace of the wait that an event is now waking up,
>>          not only stacktrace of the event.
>>      15. Make dept aware of lockdep_cmp_fn() that is used to avoid
>>          false positives in lockdep so that dept can also avoid them.
>>      16. Do do_event() only if there are no ecxts have been
>>          delimited.
>>      17. Fix a bug that was not synchronized for stage_m in struct
>>          dept_task, using a spin lock, dept_task()->stage_lock.
>>      18. Fix a bug that dept didn't handle the case that multiple
>>          ttwus for a single waiter can be called at the same time
>>          e.i. a race issue.
>>      19. Distinguish each kernel context from others, not only by
>>          system call but also by user oriented fault so that dept can
>>          work with more accuracy information about kernel context.
>>          That helps to avoid a few false positives.
>>      20. Limit dept's working to x86_64 and arm64.
>>
>> Changes from v13:
>>
>>      1. Rebase on the current latest version, v6.9-rc7.
>>      2. Add 'dept' documentation describing dept APIs.
>>
>> Changes from v12:
>>
>>      1. Refine the whole document for dept.
>>      2. Add 'Interpret dept report' section in the document, using a
>>         deadlock report obtained in practice. Hope this version of
>>         document helps guys understand dept better.
>>
>>         https://lore.kernel.org/lkml/6383cde5-cf4b-facf-6e07-1378a485657d@I-love.SAKURA.ne.jp/#t
>>         https://lore.kernel.org/lkml/1674268856-31807-1-git-send-email-byungchul.park@lge.com/
>>
>> Changes from v11:
>>
>>      1. Add 'dept' documentation describing the concept of dept.
>>      2. Rewrite the commit messages of the following commits for
>>         using weaker lockdep annotation, for better description.
>>
>>         fs/jbd2: Use a weaker annotation in journal handling
>>         cpu/hotplug: Use a weaker annotation in AP thread
>>
>>         (feedbacked by Thomas Gleixner)
>>
>> Changes from v10:
>>
>>      1. Fix noinstr warning when building kernel source.
>>      2. dept has been reporting some false positives due to the folio
>>         lock's unfairness. Reflect it and make dept work based on
>>         dept annotaions instead of just wait and wake up primitives.
>>      3. Remove the support for PG_writeback while working on 2. I
>>         will add the support later if needed.
>>      4. dept didn't print stacktrace for [S] if the participant of a
>>         deadlock is not lock mechanism but general wait and event.
>>         However, it made hard to interpret the report in that case.
>>         So add support to print stacktrace of the requestor who asked
>>         the event context to run - usually a waiter of the event does
>>         it just before going to wait state.
>>      5. Give up tracking raw_local_irq_{disable,enable}() since it
>>         totally messed up dept's irq tracking. So make it work in the
>>         same way as lockdep does. I will consider it once any false
>>         positives by those are observed again.
>>      6. Change the manual rwsem_acquire_read(->j_trans_commit_map)
>>         annotation in fs/jbd2/transaction.c to the try version so
>>         that it works as much as it exactly needs.
>>      7. Remove unnecessary 'inline' keyword in dept.c and add
>>         '__maybe_unused' to a needed place.
>>
>> Changes from v9:
>>
>>      1. Fix a bug. SDT tracking didn't work well because of my big
>>         mistake that I should've used waiter's map to indentify its
>>         class but it had been working with waker's one. FYI,
>>         PG_locked and PG_writeback weren't affected. They still
>>         worked well. (reported by YoungJun)
>>
>> Changes from v8:
>>
>>      1. Fix build error by adding EXPORT_SYMBOL(PG_locked_map) and
>>         EXPORT_SYMBOL(PG_writeback_map) for kernel module build -
>>         appologize for that. (reported by kernel test robot)
>>      2. Fix build error by removing header file's circular dependency
>>         that was caused by "atomic.h", "kernel.h" and "irqflags.h",
>>         which I introduced - appolgize for that. (reported by kernel
>>         test robot)
>>
>> Changes from v7:
>>
>>      1. Fix a bug that cannot track rwlock dependency properly,
>>         introduced in v7. (reported by Boqun and lockdep selftest)
>>      2. Track wait/event of PG_{locked,writeback} more aggressively
>>         assuming that when a bit of PG_{locked,writeback} is cleared
>>         there might be waits on the bit. (reported by Linus, Hillf
>>         and syzbot)
>>      3. Fix and clean bad style code e.i. unnecessarily introduced
>>         a randome pattern and so on. (pointed out by Linux)
>>      4. Clean code for applying dept to wait_for_completion().
>>
>> Changes from v6:
>>
>>      1. Tie to task scheduler code to track sleep and try_to_wake_up()
>>         assuming sleeps cause waits, try_to_wake_up()s would be the
>>         events that those are waiting for, of course with proper dept
>>         annotations, sdt_might_sleep_weak(), sdt_might_sleep_strong()
>>         and so on. For these cases, class is classified at sleep
>>         entrance rather than the synchronization initialization code.
>>         Which would extremely reduce false alarms.
>>      2. Remove the dept associated instance in each page struct for
>>         tracking dependencies by PG_locked and PG_writeback thanks to
>>         the 1. work above.
>>      3. Introduce CONFIG_dept_AGGRESIVE_TIMEOUT_WAIT to suppress
>>         reports that waits with timeout set are involved, for those
>>         who don't like verbose reporting.
>>      4. Add a mechanism to refill the internal memory pools on
>>         running out so that dept could keep working as long as free
>>         memory is available in the system.
>>      5. Re-enable tracking hashed-waitqueue wait. That's going to no
>>         longer generate false positives because class is classified
>>         at sleep entrance rather than the waitqueue initailization.
>>      6. Refactor to make it easier to port onto each new version of
>>         the kernel.
>>      7. Apply dept to dma fence.
>>      8. Do trivial optimizaitions.
>>
>> Changes from v5:
>>
>>      1. Use just pr_warn_once() rather than WARN_ONCE() on the lack
>>         of internal resources because WARN_*() printing stacktrace is
>>         too much for informing the lack. (feedback from Ted, Hyeonggon)
>>      2. Fix trivial bugs like missing initializing a struct before
>>         using it.
>>      3. Assign a different class per task when handling onstack
>>         variables for waitqueue or the like. Which makes dept
>>         distinguish between onstack variables of different tasks so
>>         as to prevent false positives. (reported by Hyeonggon)
>>      4. Make dept aware of even raw_local_irq_*() to prevent false
>>         positives. (reported by Hyeonggon)
>>      5. Don't consider dependencies between the events that might be
>>         triggered within __schedule() and the waits that requires
>>          __schedule(), real ones. (reported by Hyeonggon)
>>      6. Unstage the staged wait that has prepare_to_wait_event()'ed
>>         *and* yet to get to __schedule(), if we encounter __schedule()
>>         in-between for another sleep, which is possible if e.g. a
>>         mutex_lock() exists in 'condition' of ___wait_event().
>>      7. Turn on CONFIG_PROVE_LOCKING when CONFIG_DEPT is on, to rely
>>         on the hardirq and softirq entrance tracing to make dept more
>>         portable for now.
>>
>> Changes from v4:
>>
>>      1. Fix some bugs that produce false alarms.
>>      2. Distinguish each syscall context from another *for arm64*.
>>      3. Make it not warn it but just print it in case dept ring
>>         buffer gets exhausted. (feedback from Hyeonggon)
>>      4. Explicitely describe "EXPERIMENTAL" and "dept might produce
>>         false positive reports" in Kconfig. (feedback from Ted)
>>
>> Changes from v3:
>>
>>      1. dept shouldn't create dependencies between different depths
>>         of a class that were indicated by *_lock_nested(). dept
>>         normally doesn't but it does once another lock class comes
>>         in. So fixed it. (feedback from Hyeonggon)
>>      2. dept considered a wait as a real wait once getting to
>>         __schedule() even if it has been set to TASK_RUNNING by wake
>>         up sources in advance. Fixed it so that dept doesn't consider
>>         the case as a real wait. (feedback from Jan Kara)
>>      3. Stop tracking dependencies with a map once the event
>>         associated with the map has been handled. dept will start to
>>         work with the map again, on the next sleep.
>>
>> Changes from v2:
>>
>>      1. Disable dept on bit_wait_table[] in sched/wait_bit.c
>>         reporting a lot of false positives, which is my fault.
>>         Wait/event for bit_wait_table[] should've been tagged in a
>>         higher layer for better work, which is a future work.
>>         (feedback from Jan Kara)
>>      2. Disable dept on crypto_larval's completion to prevent a false
>>         positive.
>>
>> Changes from v1:
>>
>>      1. Fix coding style and typo. (feedback from Steven)
>>      2. Distinguish each work context from another in workqueue.
>>      3. Skip checking lock acquisition with nest_lock, which is about
>>         correct lock usage that should be checked by lockdep.
>>
>> Changes from RFC(v0):
>>
>>      1. Prevent adding a wait tag at prepare_to_wait() but __schedule().
>>         (feedback from Linus and Matthew)
>>      2. Use try version at lockdep_acquire_cpus_lock() annotation.
>>      3. Distinguish each syscall context from another.
>>
>> Byungchul Park (41):
>>   dept: implement DEPT(DEPendency Tracker)
>>   dept: add single event dependency tracker APIs
>>   dept: add lock dependency tracker APIs
>>   dept: tie to lockdep and IRQ tracing
>>   dept: add proc knobs to show stats and dependency graph
>>   dept: distinguish each kernel context from another
>>   dept: distinguish each work from another
>>   dept: add a mechanism to refill the internal memory pools on running
>>     out
>>   dept: record the latest one out of consecutive waits of the same class
>>   dept: apply sdt_might_sleep_{start,end}() to
>>     wait_for_completion()/complete()
>>   dept: apply sdt_might_sleep_{start,end}() to swait
>>   dept: apply sdt_might_sleep_{start,end}() to waitqueue wait
>>   dept: apply sdt_might_sleep_{start,end}() to hashed-waitqueue wait
>>   dept: apply sdt_might_sleep_{start,end}() to dma fence
>>   dept: track timeout waits separately with a new Kconfig
>>   dept: apply timeout consideration to wait_for_completion()/complete()
>>   dept: apply timeout consideration to swait
>>   dept: apply timeout consideration to waitqueue wait
>>   dept: apply timeout consideration to hashed-waitqueue wait
>>   dept: apply timeout consideration to dma fence wait
>>   dept: make dept able to work with an external wgen
>>   dept: track PG_locked with dept
>>   dept: print staged wait's stacktrace on report
>>   locking/lockdep: prevent various lockdep assertions when
>>     lockdep_off()'ed
>>   dept: add documents for dept
>>   cpu/hotplug: use a weaker annotation in AP thread
>>   dept: assign dept map to mmu notifier invalidation synchronization
>>   dept: assign unique dept_key to each distinct dma fence caller
>>   dept: make dept aware of lockdep_set_lock_cmp_fn() annotation
>>   dept: make dept stop from working on debug_locks_off()
>>   dept: assign unique dept_key to each distinct wait_for_completion()
>>     caller
>>   completion, dept: introduce init_completion_dmap() API
>>   dept: introduce a new type of dependency tracking between multi event
>>     sites
>>   dept: add module support for struct dept_event_site and
>>     dept_event_site_dep
>>   dept: introduce event_site() to disable event tracking if it's
>>     recoverable
>>   dept: implement a basic unit test for dept
>>   dept: call dept_hardirqs_off() in local_irq_*() regardless of irq
>>     state
>>   dept: introduce APIs to set page usage and use subclasses_evt for the
>>     usage
>>   dept: track PG_writeback with dept
>>   SUNRPC: relocate struct rcu_head to the first field of struct rpc_xprt
>>   mm: percpu: increase PERCPU_DYNAMIC_SIZE_SHIFT on DEPT and large
>>     PAGE_SIZE
>>
>> Yunseong Kim (1):
>>   rcu/update: fix same dept key collision between various types of RCU
>>
>>  Documentation/dev-tools/dept.rst     |  778 ++++++
>>  Documentation/dev-tools/dept_api.rst |  125 +
>>  drivers/dma-buf/dma-fence.c          |   23 +-
>>  include/asm-generic/vmlinux.lds.h    |   13 +-
>>  include/linux/completion.h           |  124 +-
>>  include/linux/dept.h                 |  402 +++
>>  include/linux/dept_ldt.h             |   78 +
>>  include/linux/dept_sdt.h             |   68 +
>>  include/linux/dept_unit_test.h       |   67 +
>>  include/linux/dma-fence.h            |   74 +-
>>  include/linux/hardirq.h              |    3 +
>>  include/linux/irq-entry-common.h     |    4 +
>>  include/linux/irqflags.h             |   21 +-
>>  include/linux/local_lock_internal.h  |    1 +
>>  include/linux/lockdep.h              |  105 +-
>>  include/linux/lockdep_types.h        |    3 +
>>  include/linux/mm_types.h             |    4 +
>>  include/linux/mmu_notifier.h         |   26 +
>>  include/linux/module.h               |    5 +
>>  include/linux/mutex.h                |    1 +
>>  include/linux/page-flags.h           |  217 +-
>>  include/linux/pagemap.h              |   37 +-
>>  include/linux/percpu-rwsem.h         |    2 +-
>>  include/linux/percpu.h               |    4 +
>>  include/linux/rcupdate_wait.h        |   13 +-
>>  include/linux/rtmutex.h              |    1 +
>>  include/linux/rwlock_types.h         |    1 +
>>  include/linux/rwsem.h                |    1 +
>>  include/linux/sched.h                |  118 +
>>  include/linux/seqlock.h              |    2 +-
>>  include/linux/spinlock_types_raw.h   |    3 +
>>  include/linux/srcu.h                 |    2 +-
>>  include/linux/sunrpc/xprt.h          |    9 +-
>>  include/linux/swait.h                |    3 +
>>  include/linux/wait.h                 |    3 +
>>  include/linux/wait_bit.h             |    3 +
>>  init/init_task.c                     |    2 +
>>  init/main.c                          |    2 +
>>  kernel/Makefile                      |    1 +
>>  kernel/cpu.c                         |    2 +-
>>  kernel/dependency/Makefile           |    5 +
>>  kernel/dependency/dept.c             | 3499 ++++++++++++++++++++++++++
>>  kernel/dependency/dept_hash.h        |   10 +
>>  kernel/dependency/dept_internal.h    |  314 +++
>>  kernel/dependency/dept_object.h      |   13 +
>>  kernel/dependency/dept_proc.c        |   94 +
>>  kernel/dependency/dept_unit_test.c   |  173 ++
>>  kernel/exit.c                        |    1 +
>>  kernel/fork.c                        |    2 +
>>  kernel/locking/lockdep.c             |   33 +
>>  kernel/module/main.c                 |   19 +
>>  kernel/rcu/rcu.h                     |    1 +
>>  kernel/rcu/update.c                  |    5 +-
>>  kernel/sched/completion.c            |   62 +-
>>  kernel/sched/core.c                  |    9 +
>>  kernel/workqueue.c                   |    3 +
>>  lib/Kconfig.debug                    |   48 +
>>  lib/debug_locks.c                    |    2 +
>>  lib/locking-selftest.c               |    2 +
>>  mm/filemap.c                         |   38 +
>>  mm/mm_init.c                         |    3 +
>>  mm/mmu_notifier.c                    |   31 +-
>>  rust/helpers/completion.c            |    5 +
>>  63 files changed, 6602 insertions(+), 121 deletions(-)
>>  create mode 100644 Documentation/dev-tools/dept.rst
>>  create mode 100644 Documentation/dev-tools/dept_api.rst
>>  create mode 100644 include/linux/dept.h
>>  create mode 100644 include/linux/dept_ldt.h
>>  create mode 100644 include/linux/dept_sdt.h
>>  create mode 100644 include/linux/dept_unit_test.h
>>  create mode 100644 kernel/dependency/Makefile
>>  create mode 100644 kernel/dependency/dept.c
>>  create mode 100644 kernel/dependency/dept_hash.h
>>  create mode 100644 kernel/dependency/dept_internal.h
>>  create mode 100644 kernel/dependency/dept_object.h
>>  create mode 100644 kernel/dependency/dept_proc.c
>>  create mode 100644 kernel/dependency/dept_unit_test.c
>>
>>
>> base-commit: 43dfc13ca972988e620a6edb72956981b75ab6b0
>> --
>> 2.17.1
>

^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox