* [PATCH 0/9] Improve process/maintainers output
From: Mauro Carvalho Chehab @ 2026-05-04 15:51 UTC (permalink / raw)
To: Jonathan Corbet, Mauro Carvalho Chehab, Miguel Ojeda
Cc: Mauro Carvalho Chehab, linux-doc, linux-kernel, rust-for-linux,
Björn Roy Baron, Alice Ryhl, Andreas Hindborg, Andrew Morton,
Benno Lossin, Boqun Feng, Danilo Krummrich, Gary Guo, Joe Perches,
Matteo Croce, Shuah Khan, Trevor Gross
Hi Jon,
As promised, this series improve the output at process/maintainers:
instead of a pure enriched text, the maintainer's file content is
now converted with a table, and has gained a javascript to allow
filtering entries.
The initial patches change the logic to split parsing from
output generation. Now, everything is stored into a dict at
the parsing phase. This way, it is easier to adjust the
directive handler for it to produce a more structured document.
Right now, the entries are sorted alphabetically, per subsystem's
name.
---
As mentioned before, I did some extra tests here, changing the sort
logic:
- per mailing lists. Easy to do but require a couple of hints;
- per number of files (after using iglob) to calculate the
actual number of patches. This is fast with NVME, but could
be slow with HDD.
Doing that helps to group the output per subsytem (assuming
that each major subsystem has its own mailing list), and
placing the subsystem before each entry. Yet, after adding
the JS filter, I'm not sure if it is worth doing it(*).
So, I kept such changes out of the final version.
In any case, with the new logic, changing the output is easy.
(*) Hint: Try setting the <Filter:> box there to linux-doc :-D
This one is sorted on a nice way, but if you set it to
linux-media, you'll see that the first entry is Orphan.
The most relevant one is hidden in the crowd.
-
It should also be easy to add some logic there at the Sphinx
directive to allow, for instance, include per-subsystem
entries, e.g. one could add, in the future, support for
things like:
.. maintainers-include::
mailing-list: linux-doc@vger.kernel.org
sort-by: file-count
And have a page inside the subsystem (Documentation on this
example) with all maintainers per subsystem.
This is just a rough idea. I'm currently not planning to
implement it.
Matteo Croce (1):
docs: escape ** glob pattern in MAINTAINERS descriptions
Mauro Carvalho Chehab (8):
docs: maintainers_include: keep hidden TOC sorted
docs: maintainers_include.py: split state machine on multiple funcs
docs: maintainers_include: cleanup the code
docs: maintainers_include.py: clean most SPHINXDIRS=process warnings
docs: maintainers_include: do some coding style cleanups
docs: maintainers_include: store maintainers entries on a dict
docs: maintainers_include: don't ignore invalid profile entries
docs: maintainers: add a filtering javascript
Documentation/sphinx/maintainers_include.py | 405 ++++++++++++--------
1 file changed, 249 insertions(+), 156 deletions(-)
--
2.54.0
^ permalink raw reply
* [PATCH 1/9] docs: maintainers_include: keep hidden TOC sorted
From: Mauro Carvalho Chehab @ 2026-05-04 15:51 UTC (permalink / raw)
To: Jonathan Corbet, Linux Doc Mailing List, Mauro Carvalho Chehab
Cc: Mauro Carvalho Chehab, linux-kernel, rust-for-linux, Shuah Khan
In-Reply-To: <cover.1777908711.git.mchehab+huawei@kernel.org>
There's no practical difference on keeping it sorted, but
it helps a lot when checking for differences after patches
to the tool.
Signed-off-by: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
---
Documentation/sphinx/maintainers_include.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py
index 1b9ece431386..c7f9911ae45b 100755
--- a/Documentation/sphinx/maintainers_include.py
+++ b/Documentation/sphinx/maintainers_include.py
@@ -265,7 +265,7 @@ class MaintainersProfile(Include):
output += "\n.. toctree::\n"
output += " :hidden:\n\n"
- for fname in maint_parser.profile_toc:
+ for fname in sorted(maint_parser.profile_toc):
output += f" {fname}\n"
output += "\n"
--
2.54.0
^ permalink raw reply related
* [PATCH 2/9] docs: escape ** glob pattern in MAINTAINERS descriptions
From: Mauro Carvalho Chehab @ 2026-05-04 15:51 UTC (permalink / raw)
To: Jonathan Corbet, Linux Doc Mailing List, Mauro Carvalho Chehab
Cc: Mauro Carvalho Chehab, linux-kernel, rust-for-linux,
Andrew Morton, Joe Perches, Matteo Croce, Shuah Khan,
Matteo Croce
In-Reply-To: <cover.1777908711.git.mchehab+huawei@kernel.org>
From: Matteo Croce <teknoraver@meta.com>
Escape '**' in the MAINTAINERS descriptions section to prevent
reStructuredText from interpreting it as bold/strong inline markup,
which causes a warning when running 'make htmldocs'.
Fixes: 420849332f9f ("get_maintainer: add ** glob pattern support")
Signed-off-by: Matteo Croce <teknoraver@meta.com>
Signed-off-by: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
---
Documentation/sphinx/maintainers_include.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Documentation/sphinx/maintainers_include.py b/Documentation/sphinx/maintainers_include.py
index c7f9911ae45b..e679acf0633d 100755
--- a/Documentation/sphinx/maintainers_include.py
+++ b/Documentation/sphinx/maintainers_include.py
@@ -127,7 +127,8 @@ class MaintainersParser:
output = None
if descriptions:
# Escape the escapes in preformatted text.
- output = "| %s" % (line.replace("\\", "\\\\"))
+ output = "| %s" % (line.replace("\\", "\\\\")
+ .replace("**", "\\**"))
# Look for and record field letter to field name mappings:
# R: Designated *reviewer*: FullName <address@domain>
m = re.search(r"\s(\S):\s", line)
--
2.54.0
^ permalink raw reply related
* [PATCH v5 4/4] kallsyms: add KUnit tests for lineinfo feature
From: Sasha Levin @ 2026-05-04 15:34 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, Zhen Lei,
Sami Tolvanen, linux-kernel, linux-kbuild, linux-modules,
linux-doc, Sasha Levin
In-Reply-To: <20260504153401.2416391-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>
---
kernel/kallsyms.c | 2 +
lib/Kconfig.debug | 10 +
lib/tests/Makefile | 3 +
lib/tests/lineinfo_kunit.c | 855 +++++++++++++++++++++++++++++++++++++
4 files changed, 870 insertions(+)
create mode 100644 lib/tests/lineinfo_kunit.c
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index 1f58b4123a8ae..6ac2786cdcbcb 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -633,6 +633,7 @@ int sprint_backtrace(char *buffer, unsigned long address)
{
return __sprint_symbol(buffer, address, -1, 1, 0, 1);
}
+EXPORT_SYMBOL_GPL(sprint_backtrace);
/**
* sprint_backtrace_build_id - Look up a backtrace symbol and return it in a text buffer
@@ -653,6 +654,7 @@ int sprint_backtrace_build_id(char *buffer, unsigned long address)
{
return __sprint_symbol(buffer, address, -1, 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 8ff5adcfe1e0a..27cb92fd131ad 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -3077,6 +3077,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 7e9c2fa52e35a..3a0100338c160 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..285d798cb6a3c
--- /dev/null
+++ b/lib/tests/lineinfo_kunit.c
@@ -0,0 +1,855 @@
+// 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);
+}
+
+/*
+ * Format a symbol with lineinfo annotation. Lineinfo is appended only
+ * via the sprint_backtrace*() entry points (kernel/kallsyms.c only adds
+ * the "(file:line)" suffix in stack-trace context — sprint_symbol() is
+ * used by %ps and many existing format strings tack literal "()" after
+ * %ps, where the annotation would render as "foo (file:line)()").
+ *
+ * sprint_backtrace() subtracts 1 from the address to handle tail-call
+ * return-address corrections; pass @addr + 1 to recover the original.
+ */
+static int sprint_with_lineinfo(char *buf, unsigned long addr)
+{
+ return sprint_backtrace(buf, addr + 1);
+}
+
+/*
+ * 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_with_lineinfo(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_with_lineinfo(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_with_lineinfo(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_with_lineinfo(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_with_lineinfo(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_with_lineinfo(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_with_lineinfo(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);
+ /*
+ * sprint_symbol() backs %ps, which existing format strings combine
+ * with literal "()" to indicate function calls; the lineinfo suffix
+ * is intentionally omitted there to avoid "foo (file:line)()".
+ */
+ KUNIT_EXPECT_FALSE_MSG(test, has_lineinfo(buf),
+ "Unexpected lineinfo in sprint_symbol output: %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);
+ /* sprint_symbol_no_offset is a sprint_symbol() variant; lineinfo is
+ * intentionally only appended in sprint_backtrace*() context.
+ */
+ KUNIT_EXPECT_FALSE_MSG(test, has_lineinfo(buf),
+ "Unexpected 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);
+ /*
+ * %pS uses sprint_symbol(), which intentionally omits the lineinfo
+ * suffix (see kernel/kallsyms.c::__sprint_symbol). Lineinfo is only
+ * added via the sprint_backtrace*() entry points, which back %pBb.
+ */
+ KUNIT_EXPECT_FALSE_MSG(test, has_lineinfo(buf),
+ "Unexpected 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);
+ /* %pSR is a sprint_symbol() variant; same rationale as %pS. */
+ KUNIT_EXPECT_FALSE_MSG(test, has_lineinfo(buf),
+ "Unexpected 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_backtrace() subtracts 1 from the input and reports offset
+ * relative to the (decremented) address, so an exact "+0x0/" can't
+ * be expected here. Verify the symbol resolves and carries lineinfo.
+ */
+ sprint_with_lineinfo(buf, addr);
+ 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 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_with_lineinfo(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_with_lineinfo(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_with_lineinfo(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_with_lineinfo(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_with_lineinfo(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_with_lineinfo(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_with_lineinfo(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);
+ /* Lineinfo is appended only via sprint_backtrace*(); the symbol
+ * variants intentionally omit it to avoid clashing with format
+ * strings that already wrap %ps in literal "()".
+ */
+ KUNIT_EXPECT_FALSE_MSG(test, has_lineinfo(buf),
+ "Unexpected 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_with_lineinfo(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_with_lineinfo output */
+ sprint_with_lineinfo(buf, addr);
+ KUNIT_ASSERT_TRUE(test, has_lineinfo(buf));
+
+ /* Try an address 8 bytes into the function (past prologue) */
+ mid_addr = addr + 8;
+ sprint_with_lineinfo(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.53.0
^ permalink raw reply related
* [PATCH v5 3/4] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
From: Sasha Levin @ 2026-05-04 15:33 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, Zhen Lei,
Sami Tolvanen, linux-kernel, linux-kbuild, linux-modules,
linux-doc, Sasha Levin
In-Reply-To: <20260504153401.2416391-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>
---
include/linux/mod_lineinfo.h | 236 ++++++++++++++++++++++++++++++-----
kernel/kallsyms.c | 46 +++----
kernel/kallsyms_internal.h | 8 +-
kernel/module/kallsyms.c | 106 +++++++---------
scripts/empty_lineinfo.S | 20 ++-
scripts/gen_lineinfo.c | 185 ++++++++++++++++++---------
scripts/kallsyms.c | 7 +-
7 files changed, 420 insertions(+), 188 deletions(-)
diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
index 9cda3263a0784..a3c7143433020 100644
--- a/include/linux/mod_lineinfo.h
+++ b/include/linux/mod_lineinfo.h
@@ -3,9 +3,9 @@
* mod_lineinfo.h - Binary format for per-module source line information
*
* This header defines the layout of the .mod_lineinfo and
- * .init.mod_lineinfo sections embedded in loadable kernel modules. It
- * is dual-use: included from both the kernel and the userspace
- * gen_lineinfo tool.
+ * .init.mod_lineinfo sections embedded in loadable kernel modules. It is
+ * dual-use: included from both the kernel and the userspace gen_lineinfo
+ * tool.
*
* Top-level layout (all values in target-native endianness):
*
@@ -20,16 +20,27 @@
* If the relocation fails to resolve (e.g. unknown reloc type), .anchor
* stays zero and lookups silently degrade to "no annotation".
*
- * Each per-section sub-table is laid out as a stand-alone
- * mod_lineinfo_header followed by parallel arrays:
+ * Each per-section sub-table is laid out exactly as a stand-alone
+ * mod_lineinfo_header followed by its arrays:
*
- * struct mod_lineinfo_header (16 bytes)
- * u32 addrs[num_entries] -- offsets from this section's 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. All offsets in the per-section
+ * header are relative to that header itself, so a sub-table is fully
+ * self-describing.
+ *
+ * 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
@@ -40,9 +51,12 @@
#include <stdint.h>
typedef uint32_t u32;
typedef uint16_t u16;
+typedef uint8_t u8;
typedef uint64_t u64;
#endif
+#define LINEINFO_BLOCK_ENTRIES 64
+
/*
* Per-section descriptor. One entry per ELF text section covered by the
* blob (.text, .exit.text, .init.text, ...).
@@ -66,39 +80,201 @@ struct mod_lineinfo_root {
struct mod_lineinfo_header {
u32 num_entries;
+ u32 num_blocks;
u32 num_files;
- u32 filenames_size; /* total bytes of concatenated filenames */
+ u32 blocks_offset; /* offset to block_addrs[] from this header */
+ 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;
};
-/* Offset helpers: compute byte offset from the per-section header 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/kernel/kallsyms.c b/kernel/kallsyms.c
index d95387f51b4c0..1f58b4123a8ae 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 819d6594c2937..2bec9f0e6afc5 100644
--- a/kernel/module/kallsyms.c
+++ b/kernel/module/kallsyms.c
@@ -498,9 +498,9 @@ int module_kallsyms_on_each_symbol(const char *modname,
#include <linux/mod_lineinfo.h>
/*
- * Search one per-section sub-table for @section_offset using flat parallel
- * arrays. @hdr is the per-section header at byte offset @hdr_offset within
- * @blob. Returns true on hit and populates @file / @line.
+ * Search one per-section sub-table for @section_offset.
+ * @hdr is the per-section header at byte offset @hdr_offset within @blob.
+ * Returns true on hit and populates @file / @line.
*/
static bool module_lookup_lineinfo_section(const void *blob, u32 blob_size,
u32 hdr_offset,
@@ -509,85 +509,71 @@ static bool module_lookup_lineinfo_section(const void *blob, u32 blob_size,
unsigned int *line)
{
const struct mod_lineinfo_header *hdr;
- const u8 *base;
- const u32 *addrs, *lines, *file_offsets;
- const u16 *file_ids;
- const char *filenames;
- u32 num_entries, num_files, filenames_size;
- unsigned int low, high, mid;
- u16 file_id;
+ struct lineinfo_table tbl;
+ const void *base;
if (hdr_offset > blob_size ||
blob_size - hdr_offset < sizeof(*hdr))
return false;
base = (const u8 *)blob + hdr_offset;
- hdr = (const struct mod_lineinfo_header *)base;
- num_entries = hdr->num_entries;
- num_files = hdr->num_files;
- filenames_size = hdr->filenames_size;
+ hdr = base;
- if (num_entries == 0)
+ if (hdr->num_entries == 0 || hdr->num_blocks == 0)
return false;
- /*
- * Validate counts before multiplying — sizing arithmetic could
- * otherwise overflow on 32-bit with a malformed blob. Each entry
- * contributes one u32 (addrs), one u16 (file_ids), and one u32
- * (lines); each file contributes one u32 (file_offsets).
- */
+ /* Validate each sub-array fits within the remaining blob bytes */
{
u32 avail = blob_size - hdr_offset;
- u32 needed = mod_lineinfo_filenames_off(num_entries, num_files);
- if (num_entries > U32_MAX / sizeof(u32))
+ if (hdr->blocks_offset > avail ||
+ hdr->blocks_size > avail - hdr->blocks_offset)
+ return false;
+ if (hdr->data_offset > avail ||
+ hdr->data_size > avail - hdr->data_offset)
return false;
- if (num_files > U32_MAX / sizeof(u32))
+ if (hdr->files_offset > avail ||
+ hdr->files_size > avail - hdr->files_offset)
return false;
- if (needed > avail || filenames_size > avail - needed)
+ if (hdr->filenames_offset > avail ||
+ hdr->filenames_size > avail - hdr->filenames_offset)
return false;
}
/*
- * Filenames are read as NUL-terminated C strings. Require the blob
- * to end in NUL so a malformed file_offsets entry can never lead the
- * later "%s" consumer past the end of the section.
+ * Validate counts before multiplying by element size — multiplication
+ * could otherwise overflow on 32-bit builds with a malformed blob.
+ * num_blocks contributes (addr,offset) u32 pairs; num_files contributes
+ * one u32 each.
*/
- if (filenames_size == 0 ||
- base[mod_lineinfo_filenames_off(num_entries, num_files) +
- filenames_size - 1] != 0)
+ if (hdr->num_blocks > hdr->blocks_size / (2 * sizeof(u32)))
return false;
-
- addrs = (const u32 *)(base + mod_lineinfo_addrs_off());
- file_ids = (const u16 *)(base + mod_lineinfo_file_ids_off(num_entries));
- lines = (const u32 *)(base + mod_lineinfo_lines_off(num_entries));
- file_offsets = (const u32 *)(base + mod_lineinfo_file_offsets_off(num_entries));
- filenames = (const char *)(base + mod_lineinfo_filenames_off(num_entries, num_files));
-
- /* Binary search for largest entry <= section_offset. */
- low = 0;
- high = num_entries;
- while (low < high) {
- mid = low + (high - low) / 2;
- if (addrs[mid] <= section_offset)
- low = mid + 1;
- else
- high = mid;
- }
-
- if (low == 0)
+ if (hdr->num_files > hdr->files_size / sizeof(u32))
return false;
- low--;
- file_id = file_ids[low];
- if (file_id >= num_files)
- return false;
- if (file_offsets[file_id] >= filenames_size)
+ /*
+ * Filenames are read as NUL-terminated C strings. Require the blob
+ * to end in NUL so a malformed file_offsets entry can never lead the
+ * later "%s" consumer past the end of the section.
+ */
+ if (hdr->filenames_size == 0 ||
+ ((const u8 *)base)[hdr->filenames_offset +
+ hdr->filenames_size - 1] != 0)
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, section_offset, file, line);
}
/*
@@ -609,6 +595,7 @@ static bool module_lookup_lineinfo_blob(const void *blob, u32 blob_size,
if (root->num_sections == 0)
return false;
+ /* Validate sections[] array fits within the blob */
if (root->num_sections > U32_MAX / sizeof(struct mod_lineinfo_section))
return false;
sections_end = sizeof(*root) +
@@ -642,6 +629,9 @@ static bool module_lookup_lineinfo_blob(const void *blob, u32 blob_size,
/*
* Look up source file:line for an address within a loaded module.
+ * Uses the .mod_lineinfo / .init.mod_lineinfo sections embedded in the .ko
+ * at build time. Each section contains one or more per-section sub-tables
+ * keyed by an ELF-relocation-resolved anchor.
*
* Safe in NMI/panic context: no locks, no allocations.
* Caller must hold RCU read lock (or be in a context where the module
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 e1e08469b4f2f..394690a23a2f7 100644
--- a/scripts/gen_lineinfo.c
+++ b/scripts/gen_lineinfo.c
@@ -825,6 +825,45 @@ static void deduplicate(struct covered_section *sections,
}
}
+/*
+ * Emit the LEB128 delta-compressed data stream for one block.
+ * @base is the absolute index of the first entry, @count is the number of
+ * entries in this block (<= LINEINFO_BLOCK_ENTRIES). Used by both vmlinux
+ * mode (one section, full entries[]) and module mode (per-section ranges).
+ */
+static void emit_block_data_range(unsigned int base, unsigned int count)
+{
+ if (!count)
+ return;
+
+ /* 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);
+ }
+}
+
+/* Vmlinux-mode wrapper: pick block index out of the global entries[]. */
+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;
+ emit_block_data_range(base, count);
+}
+
static void compute_file_offsets(void)
{
unsigned int offset = 0;
@@ -848,6 +887,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");
@@ -868,29 +912,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_addrs:\n");
- for (unsigned int i = 0; i < num_entries; i++)
- printf("\t.long 0x%x\n", entries[i].offset);
- printf("\n");
+ printf("lineinfo_num_blocks:\n");
+ printf("\t.long %u\n\n", num_blocks);
- /* 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");
+ /* Block first-addresses for binary search */
+ printf("\t.globl lineinfo_block_addrs\n");
+ printf("\t.balign 4\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);
- /* Line numbers, parallel to addrs */
- printf("\t.globl lineinfo_lines\n");
+ /* 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");
@@ -898,71 +953,81 @@ 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");
}
/*
- * Emit one per-section table in the simple flat-array layout:
+ * Emit one per-section table. @suffix uniquifies the local labels so
+ * multiple tables can coexist in a single output blob; @blob_root_label
+ * is the symbol for the start of the enclosing blob (used for
+ * table_offset = .Lhdr - .Lroot).
*
- * mod_lineinfo_header
- * addrs[count] (u32, sorted)
- * file_ids[count] (u16) + 2-byte pad if count is odd
- * lines[count] (u32)
- * file_offsets[] (u32)
- * filenames[]
- *
- * @suffix uniquifies labels so multiple tables can coexist in one blob.
- * Caller has sorted entries[] so this section's entries occupy [first,
- * first + count).
+ * Caller has already sorted entries[] so this section's entries occupy
+ * the contiguous range [first, first + count). This function emits
+ * block-relative addresses computed from entries[first + N].offset.
*/
static void emit_section_table(unsigned int first, unsigned int count,
const char *suffix)
{
+ unsigned int num_blocks;
+
+ num_blocks = count ?
+ (count + LINEINFO_BLOCK_ENTRIES - 1) / LINEINFO_BLOCK_ENTRIES : 0;
+
printf(".Lhdr%s:\n", suffix);
printf("\t.balign 4\n");
printf("\t.long %u\t\t/* num_entries */\n", count);
+ printf("\t.long %u\t\t/* num_blocks */\n", num_blocks);
printf("\t.long %u\t\t/* num_files */\n", num_files);
+ printf("\t.long .Lblk_addrs%s - .Lhdr%s\n", suffix, suffix);
+ printf("\t.long .Lblk_offsets_end%s - .Lblk_addrs%s\n", suffix, suffix);
+ printf("\t.long .Ldata%s - .Lhdr%s\n", suffix, suffix);
+ printf("\t.long .Ldata_end%s - .Ldata%s\n", suffix, suffix);
+ printf("\t.long .Lfile_offsets%s - .Lhdr%s\n", suffix, suffix);
+ printf("\t.long .Lfile_offsets_end%s - .Lfile_offsets%s\n", suffix, suffix);
+ printf("\t.long .Lfilenames%s - .Lhdr%s\n", suffix, suffix);
printf("\t.long .Lfilenames_end%s - .Lfilenames%s\n\n", suffix, suffix);
- /* addrs[] */
- for (unsigned int i = 0; i < count; i++)
- printf("\t.long 0x%x\n", entries[first + i].offset);
-
- /* file_ids[] */
- for (unsigned int i = 0; i < count; i++)
- printf("\t.short %u\n", entries[first + i].file_id);
- if (count & 1)
- printf("\t.short 0\t\t/* pad to align lines[] */\n");
-
- /* lines[] */
- for (unsigned int i = 0; i < count; i++)
- printf("\t.long %u\n", entries[first + i].line);
+ printf(".Lblk_addrs%s:\n", suffix);
+ for (unsigned int i = 0; i < num_blocks; i++)
+ printf("\t.long 0x%x\n",
+ entries[first + i * LINEINFO_BLOCK_ENTRIES].offset);
+
+ printf(".Lblk_offsets%s:\n", suffix);
+ for (unsigned int i = 0; i < num_blocks; i++)
+ printf("\t.long .Lblock%s_%u - .Ldata%s\n", suffix, i, suffix);
+ printf(".Lblk_offsets_end%s:\n\n", suffix);
+
+ printf(".Ldata%s:\n", suffix);
+ for (unsigned int i = 0; i < num_blocks; i++) {
+ unsigned int base = first + i * LINEINFO_BLOCK_ENTRIES;
+ unsigned int n = count - i * LINEINFO_BLOCK_ENTRIES;
+
+ if (n > LINEINFO_BLOCK_ENTRIES)
+ n = LINEINFO_BLOCK_ENTRIES;
+ printf(".Lblock%s_%u:\n", suffix, i);
+ emit_block_data_range(base, n);
+ }
+ printf(".Ldata_end%s:\n", suffix);
- /* file_offsets[] */
printf("\t.balign 4\n");
+ printf(".Lfile_offsets%s:\n", suffix);
for (unsigned int i = 0; i < num_files; i++)
printf("\t.long %u\n", files[i].str_offset);
+ printf(".Lfile_offsets_end%s:\n\n", suffix);
- /* filenames[] */
printf(".Lfilenames%s:\n", suffix);
for (unsigned int i = 0; i < num_files; i++)
print_escaped_asciz(files[i].name);
@@ -1236,8 +1301,10 @@ int main(int argc, char *argv[])
deduplicate(NULL, 0);
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);
output_assembly();
}
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.53.0
^ permalink raw reply related
* [PATCH v5 2/4] kallsyms: extend lineinfo to loadable modules
From: Sasha Levin @ 2026-05-04 15:33 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, Zhen Lei,
Sami Tolvanen, linux-kernel, linux-kbuild, linux-modules,
linux-doc, Sasha Levin
In-Reply-To: <20260504153401.2416391-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 per-section .mod_lineinfo and .init.mod_lineinfo
sections containing compact binary tables of section-relative offsets,
file IDs, line numbers, and filenames, and embeds them back into the
.ko via a partial link (ld -r).
At runtime, module_lookup_lineinfo() walks the section descriptors in
each blob, finds the one whose runtime range contains the queried
address, and binary-searches that section's table. 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
(or its init memory) is unloaded.
The gen_lineinfo tool gains --module mode which:
- Walks an allowlist of text-like sections (.text, .exit.text,
.init.text), gating each on its presence in the .ko.
- Uses an ELF relocation against each covered section's symbol as the
runtime "anchor", resolved by the module loader's standard
apply_relocations() pass — no implicit base derivation from
mod->mem[].base, no special-cased loader logic.
- Disambiguates DWARF addresses across sections that all share
sh_addr == 0 in ET_REL files via per-section synthetic biases
applied inside apply_debug_line_relocations() (handles both abs32
and abs64 width relocs).
- Handles libdw's ET_REL path-doubling quirk in make_relative().
- Declares empty section stanzas in its output assembly so the
resulting lineinfo.o has LOCAL SECTION symbols rather than GLOBAL
UND ones; otherwise ld -r would not bind the relocation to the
.ko's existing section symbol of the same name and depmod would
warn.
The build pipeline runs gen-mod-lineinfo.sh after the existing modfinal
step:
gen_lineinfo --module ${KO} > ${KO}.lineinfo.S
${CC} -c -o ${KO}.lineinfo.o ${KO}.lineinfo.S
${LD} -r ${KO}.lineinfo.o ${KO} -o ${KO}.tmp && mv ${KO}.tmp ${KO}
Order matters: lineinfo.o must come first so its zero-byte text
contributions stay at offset 0 of the merged sections.
The init blob lives in MOD_INIT_RODATA and is revoked via WRITE_ONCE
in do_init_module() before do_free_init() releases the memory; the
module_init_lineinfo_data() reader uses READ_ONCE so concurrent
lookups either see the old pointer (still valid until do_free_init's
synchronize_rcu) or NULL.
The struct module fields are guarded by
#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES and accessed through inline
reader accessors so callers don't duplicate the guard.
Per-module overhead is approximately 14 bytes per DWARF line entry
plus a small fixed cost per covered section descriptor. The next
patch in this series delta-compresses the per-section streams to ~3-4
bytes per entry.
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
.../admin-guide/kallsyms-lineinfo.rst | 41 +-
MAINTAINERS | 108 ++-
include/linux/mod_lineinfo.h | 104 +++
include/linux/module.h | 39 +
init/Kconfig | 19 +-
kernel/kallsyms.c | 18 +-
kernel/module/kallsyms.c | 181 ++++
kernel/module/main.c | 26 +
scripts/Makefile.modfinal | 6 +
scripts/gen-mod-lineinfo.sh | 50 +
scripts/gen_lineinfo.c | 854 ++++++++++++++++--
11 files changed, 1320 insertions(+), 126 deletions(-)
create mode 100644 include/linux/mod_lineinfo.h
create mode 100644 scripts/gen-mod-lineinfo.sh
diff --git a/Documentation/admin-guide/kallsyms-lineinfo.rst b/Documentation/admin-guide/kallsyms-lineinfo.rst
index c8ec124394354..dd264830c8d5b 100644
--- a/Documentation/admin-guide/kallsyms-lineinfo.rst
+++ b/Documentation/admin-guide/kallsyms-lineinfo.rst
@@ -51,22 +51,47 @@ 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 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 2-3 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 a683a7023f6e9..f264e763d1041 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -7873,7 +7873,7 @@ F: drivers/gpu/drm/sun4i/sun8i*
DRM DRIVER FOR APPLE TOUCH BARS
M: Aun-Ali Zaidi <admin@kodeit.net>
-M: Aditya Garg <gargaditya08@live.com>
+M: Aditya Garg <gargaditya08@proton.me>
L: dri-devel@lists.freedesktop.org
S: Maintained
T: git https://gitlab.freedesktop.org/drm/misc/kernel.git
@@ -13797,6 +13797,9 @@ KALLSYMS LINEINFO
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
KASAN
@@ -13866,7 +13869,6 @@ M: Pratyush Yadav <pratyush@kernel.org>
R: Dave Young <ruirui.yang@linux.dev>
L: kexec@lists.infradead.org
S: Maintained
-W: http://lse.sourceforge.net/kdump/
F: Documentation/admin-guide/kdump/
F: fs/proc/vmcore.c
F: include/linux/crash_core.h
@@ -15258,7 +15260,7 @@ M: Andrea Cervesato <andrea.cervesato@suse.com>
M: Cyril Hrubis <chrubis@suse.cz>
M: Jan Stancek <jstancek@redhat.com>
M: Petr Vorel <pvorel@suse.cz>
-M: Li Wang <liwang@redhat.com>
+M: Li Wang <li.wang@linux.dev>
M: Yang Xu <xuyang2018.jy@fujitsu.com>
M: Xiao Yang <yangx.jy@fujitsu.com>
L: ltp@lists.linux.it (subscribers-only)
@@ -15405,7 +15407,7 @@ F: include/net/netns/mctp.h
F: net/mctp/
MAPLE TREE
-M: Liam R. Howlett <Liam.Howlett@oracle.com>
+M: Liam R. Howlett <liam@infradead.org>
R: Alice Ryhl <aliceryhl@google.com>
R: Andrew Ballance <andrewjballance@gmail.com>
L: maple-tree@lists.infradead.org
@@ -16765,7 +16767,7 @@ MEMORY MANAGEMENT - CORE
M: Andrew Morton <akpm@linux-foundation.org>
M: David Hildenbrand <david@kernel.org>
R: Lorenzo Stoakes <ljs@kernel.org>
-R: Liam R. Howlett <Liam.Howlett@oracle.com>
+R: Liam R. Howlett <liam@infradead.org>
R: Vlastimil Babka <vbabka@kernel.org>
R: Mike Rapoport <rppt@kernel.org>
R: Suren Baghdasaryan <surenb@google.com>
@@ -16811,7 +16813,7 @@ F: mm/sparse.c
F: mm/util.c
F: mm/vmpressure.c
F: mm/vmstat.c
-N: include/linux/page[-_]*
+N: include\/linux\/page[-_][a-zA-Z]*
MEMORY MANAGEMENT - EXECMEM
M: Andrew Morton <akpm@linux-foundation.org>
@@ -16901,7 +16903,7 @@ MEMORY MANAGEMENT - MISC
M: Andrew Morton <akpm@linux-foundation.org>
M: David Hildenbrand <david@kernel.org>
R: Lorenzo Stoakes <ljs@kernel.org>
-R: Liam R. Howlett <Liam.Howlett@oracle.com>
+R: Liam R. Howlett <liam@infradead.org>
R: Vlastimil Babka <vbabka@kernel.org>
R: Mike Rapoport <rppt@kernel.org>
R: Suren Baghdasaryan <surenb@google.com>
@@ -16968,6 +16970,7 @@ S: Maintained
F: include/linux/compaction.h
F: include/linux/gfp.h
F: include/linux/page-isolation.h
+F: include/linux/pageblock-flags.h
F: mm/compaction.c
F: mm/debug_page_alloc.c
F: mm/debug_page_ref.c
@@ -16989,7 +16992,7 @@ M: Andrew Morton <akpm@linux-foundation.org>
M: Johannes Weiner <hannes@cmpxchg.org>
R: David Hildenbrand <david@kernel.org>
R: Michal Hocko <mhocko@kernel.org>
-R: Qi Zheng <zhengqi.arch@bytedance.com>
+R: Qi Zheng <qi.zheng@linux.dev>
R: Shakeel Butt <shakeel.butt@linux.dev>
R: Lorenzo Stoakes <ljs@kernel.org>
L: linux-mm@kvack.org
@@ -17002,7 +17005,7 @@ M: Andrew Morton <akpm@linux-foundation.org>
M: David Hildenbrand <david@kernel.org>
M: Lorenzo Stoakes <ljs@kernel.org>
R: Rik van Riel <riel@surriel.com>
-R: Liam R. Howlett <Liam.Howlett@oracle.com>
+R: Liam R. Howlett <liam@infradead.org>
R: Vlastimil Babka <vbabka@kernel.org>
R: Harry Yoo <harry@kernel.org>
R: Jann Horn <jannh@google.com>
@@ -17049,7 +17052,7 @@ M: David Hildenbrand <david@kernel.org>
M: Lorenzo Stoakes <ljs@kernel.org>
R: Zi Yan <ziy@nvidia.com>
R: Baolin Wang <baolin.wang@linux.alibaba.com>
-R: Liam R. Howlett <Liam.Howlett@oracle.com>
+R: Liam R. Howlett <liam@infradead.org>
R: Nico Pache <npache@redhat.com>
R: Ryan Roberts <ryan.roberts@arm.com>
R: Dev Jain <dev.jain@arm.com>
@@ -17087,7 +17090,7 @@ F: tools/testing/selftests/mm/uffd-*.[ch]
MEMORY MANAGEMENT - RUST
M: Alice Ryhl <aliceryhl@google.com>
R: Lorenzo Stoakes <ljs@kernel.org>
-R: Liam R. Howlett <Liam.Howlett@oracle.com>
+R: Liam R. Howlett <liam@infradead.org>
L: linux-mm@kvack.org
L: rust-for-linux@vger.kernel.org
S: Maintained
@@ -17101,7 +17104,7 @@ F: rust/kernel/page.rs
MEMORY MAPPING
M: Andrew Morton <akpm@linux-foundation.org>
-M: Liam R. Howlett <Liam.Howlett@oracle.com>
+M: Liam R. Howlett <liam@infradead.org>
M: Lorenzo Stoakes <ljs@kernel.org>
R: Vlastimil Babka <vbabka@kernel.org>
R: Jann Horn <jannh@google.com>
@@ -17133,7 +17136,7 @@ F: tools/testing/vma/
MEMORY MAPPING - LOCKING
M: Andrew Morton <akpm@linux-foundation.org>
M: Suren Baghdasaryan <surenb@google.com>
-M: Liam R. Howlett <Liam.Howlett@oracle.com>
+M: Liam R. Howlett <liam@infradead.org>
M: Lorenzo Stoakes <ljs@kernel.org>
R: Vlastimil Babka <vbabka@kernel.org>
R: Shakeel Butt <shakeel.butt@linux.dev>
@@ -17148,7 +17151,7 @@ F: mm/mmap_lock.c
MEMORY MAPPING - MADVISE (MEMORY ADVICE)
M: Andrew Morton <akpm@linux-foundation.org>
-M: Liam R. Howlett <Liam.Howlett@oracle.com>
+M: Liam R. Howlett <liam@infradead.org>
M: Lorenzo Stoakes <ljs@kernel.org>
M: David Hildenbrand <david@kernel.org>
R: Vlastimil Babka <vbabka@kernel.org>
@@ -18678,19 +18681,59 @@ F: net/xfrm/
F: tools/testing/selftests/net/ipsec.c
NETWORKING [IPv4/IPv6]
-M: "David S. Miller" <davem@davemloft.net>
M: David Ahern <dsahern@kernel.org>
+M: Ido Schimmel <idosch@nvidia.com>
L: netdev@vger.kernel.org
S: Maintained
-T: git git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git
-F: arch/x86/net/*
-F: include/linux/ip.h
-F: include/linux/ipv6*
+F: Documentation/netlink/specs/rt-addr.yaml
+F: Documentation/netlink/specs/rt-neigh.yaml
+F: Documentation/netlink/specs/rt-route.yaml
+F: Documentation/netlink/specs/rt-rule.yaml
+F: include/linux/inetdevice.h
+F: include/linux/mroute*
+F: include/net/addrconf.h
+F: include/net/arp.h
F: include/net/fib*
+F: include/net/if_inet6.h
+F: include/net/inetpeer.h
F: include/net/ip*
+F: include/net/lwtunnel.h
+F: include/net/ndisc.h
+F: include/net/netns/nexthop.h
+F: include/net/nexthop.h
F: include/net/route.h
-F: net/ipv4/
-F: net/ipv6/
+F: include/uapi/linux/fib_rules.h
+F: include/uapi/linux/in_route.h
+F: include/uapi/linux/mroute*
+F: include/uapi/linux/nexthop.h
+F: net/core/fib*
+F: net/core/lwtunnel.c
+F: net/ipv4/arp.c
+F: net/ipv4/devinet.c
+F: net/ipv4/fib*
+F: net/ipv4/icmp.c
+F: net/ipv4/igmp.c
+F: net/ipv4/inet_fragment.c
+F: net/ipv4/inetpeer.c
+F: net/ipv4/ip*
+F: net/ipv4/metrics.c
+F: net/ipv4/netlink.c
+F: net/ipv4/nexthop.c
+F: net/ipv4/route.c
+F: net/ipv6/addr*
+F: net/ipv6/anycast.c
+F: net/ipv6/exthdrs.c
+F: net/ipv6/exthdrs_core.c
+F: net/ipv6/fib*
+F: net/ipv6/icmp.c
+F: net/ipv6/ip*
+F: net/ipv6/mcast*
+F: net/ipv6/ndisc.c
+F: net/ipv6/output_core.c
+F: net/ipv6/reassembly.c
+F: net/ipv6/route.c
+F: tools/testing/selftests/net/fib*
+F: tools/testing/selftests/net/forwarding/
NETWORKING [LABELED] (NetLabel, Labeled IPsec, SECMARK)
M: Paul Moore <paul@paul-moore.com>
@@ -18825,18 +18868,11 @@ F: Documentation/networking/net_failover.rst
F: drivers/net/net_failover.c
F: include/net/net_failover.h
-NEXTHOP
-M: David Ahern <dsahern@kernel.org>
-L: netdev@vger.kernel.org
-S: Maintained
-F: include/net/netns/nexthop.h
-F: include/net/nexthop.h
-F: include/uapi/linux/nexthop.h
-F: net/ipv4/nexthop.c
-
NFC SUBSYSTEM
-L: netdev@vger.kernel.org
-S: Orphan
+M: David Heidelberg <david+nfc@ixit.cz>
+L: oe-linux-nfc@lists.linux.dev
+S: Maintained
+T: git https://codeberg.org/linux-nfc/linux.git
F: Documentation/devicetree/bindings/net/nfc/
F: drivers/nfc/
F: include/net/nfc/
@@ -20780,6 +20816,7 @@ M: Dominik Brodowski <linux@dominikbrodowski.net>
S: Odd Fixes
T: git git://git.kernel.org/pub/scm/linux/kernel/git/brodo/linux.git
F: Documentation/pcmcia/
+F: drivers/net/ethernet/8390/pcnet_cs.c
F: drivers/pcmcia/
F: include/pcmcia/
F: tools/pcmcia/
@@ -23375,7 +23412,7 @@ RUST [ALLOC]
M: Danilo Krummrich <dakr@kernel.org>
R: Lorenzo Stoakes <ljs@kernel.org>
R: Vlastimil Babka <vbabka@kernel.org>
-R: Liam R. Howlett <Liam.Howlett@oracle.com>
+R: Liam R. Howlett <liam@infradead.org>
R: Uladzislau Rezki <urezki@gmail.com>
L: rust-for-linux@vger.kernel.org
S: Maintained
@@ -23527,7 +23564,7 @@ F: drivers/s390/net/
S390 PCI SUBSYSTEM
M: Niklas Schnelle <schnelle@linux.ibm.com>
-M: Gerald Schaefer <gerald.schaefer@linux.ibm.com>
+M: Gerd Bayer <gbayer@linux.ibm.com>
L: linux-s390@vger.kernel.org
S: Supported
F: Documentation/arch/s390/pci.rst
@@ -24320,7 +24357,7 @@ F: include/media/i2c/rj54n1cb0c.h
SHRINKER
M: Andrew Morton <akpm@linux-foundation.org>
M: Dave Chinner <david@fromorbit.com>
-R: Qi Zheng <zhengqi.arch@bytedance.com>
+R: Qi Zheng <qi.zheng@linux.dev>
R: Roman Gushchin <roman.gushchin@linux.dev>
R: Muchun Song <muchun.song@linux.dev>
L: linux-mm@kvack.org
@@ -24770,6 +24807,7 @@ SOFTWARE RAID (Multiple Disks) SUPPORT
M: Song Liu <song@kernel.org>
M: Yu Kuai <yukuai@fnnas.com>
R: Li Nan <linan122@huawei.com>
+R: Xiao Ni <xiao@kernel.org>
L: linux-raid@vger.kernel.org
S: Supported
Q: https://patchwork.kernel.org/project/linux-raid/list/
diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
new file mode 100644
index 0000000000000..9cda3263a0784
--- /dev/null
+++ b/include/linux/mod_lineinfo.h
@@ -0,0 +1,104 @@
+/* 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 and
+ * .init.mod_lineinfo sections embedded in loadable kernel modules. It
+ * is dual-use: included from both the kernel and the userspace
+ * gen_lineinfo tool.
+ *
+ * Top-level layout (all values in target-native endianness):
+ *
+ * struct mod_lineinfo_root
+ * struct mod_lineinfo_section sections[hdr.num_sections]
+ * ... per-section sub-tables, each pointed at by sections[i].table_offset
+ *
+ * Each mod_lineinfo_section descriptor identifies one ELF text section
+ * covered by the lineinfo blob. Its .anchor field is an ELF relocation
+ * resolved at module-load time to the runtime base of the named section,
+ * eliminating the need to derive the base from mod->mem[].base segments.
+ * If the relocation fails to resolve (e.g. unknown reloc type), .anchor
+ * stays zero and lookups silently degrade to "no annotation".
+ *
+ * Each per-section sub-table is laid out as a stand-alone
+ * mod_lineinfo_header followed by parallel arrays:
+ *
+ * struct mod_lineinfo_header (16 bytes)
+ * u32 addrs[num_entries] -- offsets from this section's 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;
+typedef uint64_t u64;
+#endif
+
+/*
+ * Per-section descriptor. One entry per ELF text section covered by the
+ * blob (.text, .exit.text, .init.text, ...).
+ */
+struct mod_lineinfo_section {
+ u64 anchor; /* RELOC: runtime base of covered section, or 0 */
+ u32 size; /* covered section size in bytes */
+ u32 table_offset; /* byte offset from blob start to this section's
+ * mod_lineinfo_header */
+};
+
+/*
+ * Top-level header. Sits at offset 0 of every .mod_lineinfo /
+ * .init.mod_lineinfo section. The compiler inserts 4 bytes of trailing
+ * padding so the u64 anchor in sections[0] starts 8-byte aligned.
+ */
+struct mod_lineinfo_root {
+ u32 num_sections;
+ struct mod_lineinfo_section sections[];
+};
+
+struct mod_lineinfo_header {
+ u32 num_entries;
+ u32 num_files;
+ u32 filenames_size; /* total bytes of concatenated filenames */
+};
+
+/* Offset helpers: compute byte offset from the per-section header 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 7566815fabbe8..2bc0263b086d2 100644
--- a/include/linux/module.h
+++ b/include/linux/module.h
@@ -507,6 +507,12 @@ struct module {
void *btf_data;
void *btf_base_data;
#endif
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+ void *lineinfo_data; /* .mod_lineinfo section in MOD_RODATA */
+ unsigned int lineinfo_data_size;
+ void *init_lineinfo_data; /* .init.mod_lineinfo, NULL after init runs */
+ unsigned int init_lineinfo_data_size;
+#endif
#ifdef CONFIG_JUMP_LABEL
struct jump_entry *jump_entries;
unsigned int num_jump_entries;
@@ -1020,6 +1026,39 @@ 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);
+
+/*
+ * Reader accessors so callers don't need to duplicate the
+ * CONFIG_KALLSYMS_LINEINFO_MODULES guard around mod->lineinfo_data /
+ * mod->init_lineinfo_data field access. Setters/clearers in the loader
+ * use the field directly under a matching #ifdef.
+ */
+static inline void *module_lineinfo_data(const struct module *mod,
+ unsigned int *size)
+{
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+ *size = mod->lineinfo_data_size;
+ return mod->lineinfo_data;
+#else
+ *size = 0;
+ return NULL;
+#endif
+}
+
+static inline void *module_init_lineinfo_data(const struct module *mod,
+ unsigned int *size)
+{
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+ *size = READ_ONCE(mod->init_lineinfo_data_size);
+ return READ_ONCE(mod->init_lineinfo_data);
+#else
+ *size = 0;
+ return NULL;
+#endif
+}
+
/* 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 99e78c253056a..3e3acfc37be7e 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2085,8 +2085,23 @@ 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.
+
+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 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 1e3f527b13988..d95387f51b4c0 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -551,12 +551,24 @@ static int __sprint_symbol(char *buffer, unsigned long address,
* replaced with bar()"); appending lineinfo there would produce a
* confusing "foo (file:line)()".
*/
- if (add_lineinfo && IS_ENABLED(CONFIG_KALLSYMS_LINEINFO) && !modname) {
+ if (add_lineinfo && 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..819d6594c2937 100644
--- a/kernel/module/kallsyms.c
+++ b/kernel/module/kallsyms.c
@@ -494,3 +494,184 @@ int module_kallsyms_on_each_symbol(const char *modname,
mutex_unlock(&module_mutex);
return ret;
}
+
+#include <linux/mod_lineinfo.h>
+
+/*
+ * Search one per-section sub-table for @section_offset using flat parallel
+ * arrays. @hdr is the per-section header at byte offset @hdr_offset within
+ * @blob. Returns true on hit and populates @file / @line.
+ */
+static bool module_lookup_lineinfo_section(const void *blob, u32 blob_size,
+ u32 hdr_offset,
+ unsigned int section_offset,
+ const char **file,
+ unsigned int *line)
+{
+ const struct mod_lineinfo_header *hdr;
+ const u8 *base;
+ const u32 *addrs, *lines, *file_offsets;
+ const u16 *file_ids;
+ const char *filenames;
+ u32 num_entries, num_files, filenames_size;
+ unsigned int low, high, mid;
+ u16 file_id;
+
+ if (hdr_offset > blob_size ||
+ blob_size - hdr_offset < sizeof(*hdr))
+ return false;
+
+ base = (const u8 *)blob + hdr_offset;
+ hdr = (const struct mod_lineinfo_header *)base;
+ num_entries = hdr->num_entries;
+ num_files = hdr->num_files;
+ filenames_size = hdr->filenames_size;
+
+ if (num_entries == 0)
+ return false;
+
+ /*
+ * Validate counts before multiplying — sizing arithmetic could
+ * otherwise overflow on 32-bit with a malformed blob. Each entry
+ * contributes one u32 (addrs), one u16 (file_ids), and one u32
+ * (lines); each file contributes one u32 (file_offsets).
+ */
+ {
+ u32 avail = blob_size - hdr_offset;
+ u32 needed = mod_lineinfo_filenames_off(num_entries, num_files);
+
+ if (num_entries > U32_MAX / sizeof(u32))
+ return false;
+ if (num_files > U32_MAX / sizeof(u32))
+ return false;
+ if (needed > avail || filenames_size > avail - needed)
+ return false;
+ }
+
+ /*
+ * Filenames are read as NUL-terminated C strings. Require the blob
+ * to end in NUL so a malformed file_offsets entry can never lead the
+ * later "%s" consumer past the end of the section.
+ */
+ if (filenames_size == 0 ||
+ base[mod_lineinfo_filenames_off(num_entries, num_files) +
+ filenames_size - 1] != 0)
+ return false;
+
+ addrs = (const u32 *)(base + mod_lineinfo_addrs_off());
+ file_ids = (const u16 *)(base + mod_lineinfo_file_ids_off(num_entries));
+ lines = (const u32 *)(base + mod_lineinfo_lines_off(num_entries));
+ file_offsets = (const u32 *)(base + mod_lineinfo_file_offsets_off(num_entries));
+ filenames = (const char *)(base + mod_lineinfo_filenames_off(num_entries, num_files));
+
+ /* Binary search for largest entry <= section_offset. */
+ low = 0;
+ high = num_entries;
+ while (low < high) {
+ mid = low + (high - low) / 2;
+ if (addrs[mid] <= section_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;
+}
+
+/*
+ * Walk a single .mod_lineinfo / .init.mod_lineinfo blob, find the section
+ * descriptor whose [anchor, anchor+size) range contains @addr, then search
+ * that section's sub-table.
+ */
+static bool module_lookup_lineinfo_blob(const void *blob, u32 blob_size,
+ unsigned long addr,
+ const char **file, unsigned int *line)
+{
+ const struct mod_lineinfo_root *root;
+ u32 i, sections_end;
+
+ if (!blob || blob_size < sizeof(*root))
+ return false;
+
+ root = blob;
+ if (root->num_sections == 0)
+ return false;
+
+ if (root->num_sections > U32_MAX / sizeof(struct mod_lineinfo_section))
+ return false;
+ sections_end = sizeof(*root) +
+ root->num_sections * sizeof(struct mod_lineinfo_section);
+ if (sections_end > blob_size)
+ return false;
+
+ for (i = 0; i < root->num_sections; i++) {
+ const struct mod_lineinfo_section *s = &root->sections[i];
+ unsigned long base = (unsigned long)s->anchor;
+ unsigned long offset;
+
+ if (!base)
+ continue; /* relocation didn't resolve */
+ if (addr < base)
+ continue;
+ offset = addr - base;
+ if (offset >= s->size)
+ continue;
+ if (offset > U32_MAX)
+ continue;
+
+ return module_lookup_lineinfo_section(blob, blob_size,
+ s->table_offset,
+ (unsigned int)offset,
+ file, line);
+ }
+
+ return false;
+}
+
+/*
+ * Look up source file:line for an address within a loaded module.
+ *
+ * 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 void *blob;
+ unsigned int size;
+
+ if (!IS_ENABLED(CONFIG_KALLSYMS_LINEINFO_MODULES))
+ return false;
+
+ blob = module_lineinfo_data(mod, &size);
+ if (blob && module_lookup_lineinfo_blob(blob, size, addr, file, line))
+ return true;
+
+ /*
+ * The init blob lives in MOD_INIT_RODATA and is revoked by
+ * do_init_module() before do_free_init() releases the memory. The
+ * READ_ONCE inside module_init_lineinfo_data() pairs with the
+ * WRITE_ONCE in do_init_module so we never see a partial
+ * pointer/size pair, and an RCU grace period in do_free_init()
+ * guarantees the memory still exists for the duration of any lookup
+ * that captured the pointer before the revocation.
+ */
+ blob = module_init_lineinfo_data(mod, &size);
+ if (blob && module_lookup_lineinfo_blob(blob, size, addr, file, line))
+ return true;
+
+ return false;
+}
diff --git a/kernel/module/main.c b/kernel/module/main.c
index 46dd8d25a6058..46bb2bf799d1e 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2712,6 +2712,19 @@ 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
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+ /*
+ * Use section_objs() (not any_section_objs) — both blobs carry an
+ * ELF anchor relocation that the module loader resolves via its
+ * standard apply_relocations() pass, which only walks SHF_ALLOC
+ * sections. Picking up a non-ALLOC section here would also leave
+ * the pointer dangling into the temporary load image once freed.
+ */
+ mod->lineinfo_data = section_objs(info, ".mod_lineinfo", 1,
+ &mod->lineinfo_data_size);
+ mod->init_lineinfo_data = section_objs(info, ".init.mod_lineinfo", 1,
+ &mod->init_lineinfo_data_size);
+#endif
#ifdef CONFIG_JUMP_LABEL
mod->jump_entries = section_objs(info, "__jump_table",
sizeof(*mod->jump_entries),
@@ -3165,6 +3178,19 @@ static noinline int do_init_module(struct module *mod)
/* .BTF is not SHF_ALLOC and will get removed, so sanitize pointers */
mod->btf_data = NULL;
mod->btf_base_data = NULL;
+#endif
+#ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
+ /*
+ * .init.mod_lineinfo lives in MOD_INIT_RODATA which do_free_init() is
+ * about to release. Clear the pointer so concurrent stack-trace
+ * lookups stop dereferencing it; do_free_init()'s synchronize_rcu()
+ * then waits out any reader that already captured the old pointer.
+ * WRITE_ONCE pairs with the READ_ONCE inside module_init_lineinfo_data()
+ * so the compiler can't tear or reorder the revocation across the
+ * llist_add() that follows.
+ */
+ WRITE_ONCE(mod->init_lineinfo_data, NULL);
+ WRITE_ONCE(mod->init_lineinfo_data_size, 0);
#endif
/*
* We want to free module_init, but be aware that kallsyms may be
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 100644
index 0000000000000..832d290f3bf4c
--- /dev/null
+++ b/scripts/gen-mod-lineinfo.sh
@@ -0,0 +1,50 @@
+#!/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 that contains
+# an ELF relocation against the module's .text section symbol, and partial-
+# links the result back into the .ko via "ld -r" so the relocation rides
+# along to the module loader. 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.tmp"
+}
+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"
+
+# Partial-link lineinfo.o INTO the .ko. Order matters: lineinfo.o must come
+# FIRST so its empty .text contributes 0 bytes at offset 0 of the merged
+# .text, which keeps the .quad .text relocation (against lineinfo.o's local
+# .text symbol, which after merge points at offset 0 of merged .text)
+# resolving to the start of the module's .text. Reversing inputs here
+# silently breaks lookup correctness.
+${LD} -r "${KO}.lineinfo.o" "${KO}" -o "${KO}.lineinfo.tmp"
+mv "${KO}.lineinfo.tmp" "${KO}"
+
+exit 0
diff --git a/scripts/gen_lineinfo.c b/scripts/gen_lineinfo.c
index 699e760178f09..e1e08469b4f2f 100644
--- a/scripts/gen_lineinfo.c
+++ b/scripts/gen_lineinfo.c
@@ -24,16 +24,66 @@
#include <gelf.h>
#include <limits.h>
+#include "../include/linux/mod_lineinfo.h"
+
+static int module_mode;
+
static unsigned int skipped_overflow;
+/* Target ELF traits, captured once in main() and reused at emit time. */
+static bool target_64bit;
+static bool target_le;
+
/*
- * vmlinux mode: end of the invariant .text region. Zero means "no cap"
- * (graceful fallback when _etext is absent on some build).
+ * Vmlinux mode only: address range of the *invariant* .text region.
+ * See find_text_end_addr() for why we cap on _etext. text_end_addr == 0
+ * means "no cap available; capture everything above text_addr" (v3
+ * behavior, used as graceful fallback if _etext is absent).
*/
static unsigned long long text_end_addr;
+/*
+ * In module mode we cover several text-like sections, split across two
+ * output blobs by lifecycle:
+ *
+ * .mod_lineinfo -- persistent code (.text, .exit.text); MOD_RODATA
+ * .init.mod_lineinfo -- init code (.init.text); freed with init memory
+ *
+ * In ET_REL .ko files .text/.init.text/.exit.text all have sh_addr == 0,
+ * so DWARF line addresses (which become sh_addr + addend after relocation)
+ * collide across sections. We disambiguate by giving each *present*
+ * covered section a unique synthetic "bias" — a u32 base address — and
+ * adding that bias to relocated values inside apply_debug_line_relocations.
+ * libdw then yields biased addresses that classify_address() can map back
+ * to a single section unambiguously. The bias is internal to gen_lineinfo
+ * and never leaks into the emitted blob.
+ */
+enum mod_lineinfo_blob {
+ BLOB_PERSISTENT,
+ BLOB_INIT,
+ NUM_BLOBS,
+};
+
+struct covered_section {
+ const char *name; /* ELF section name (e.g. ".text") */
+ enum mod_lineinfo_blob blob;
+ unsigned long long bias;/* synthetic base address (set in resolve_*) */
+ unsigned long long size;
+ bool present; /* found in this .ko */
+ unsigned int sec_index; /* ELF section header index, for reloc matching */
+ unsigned int n_entries; /* DWARF line entries collected for this section */
+};
+
+static struct covered_section all_sections[] = {
+ { .name = ".text", .blob = BLOB_PERSISTENT },
+ { .name = ".exit.text", .blob = BLOB_PERSISTENT },
+ { .name = ".init.text", .blob = BLOB_INIT },
+};
+#define ALL_SECTIONS (sizeof(all_sections) / sizeof(all_sections[0]))
+
struct line_entry {
- unsigned int offset; /* offset from _text */
+ unsigned int offset; /* offset from covered section's start */
+ unsigned int section_id;/* index into covered_sections[] (module mode only) */
unsigned int file_id;
unsigned int line;
};
@@ -52,7 +102,12 @@ static struct file_entry *files;
static unsigned int num_files;
static unsigned int files_capacity;
-#define FILE_HASH_BITS 13
+/*
+ * Hash size must comfortably exceed the 65535-file cap below so the open
+ * addressing in find_or_add_file() always has a free slot to land on.
+ * 17 bits = 131072 entries gives ~50% max load factor.
+ */
+#define FILE_HASH_BITS 17
#define FILE_HASH_SIZE (1 << FILE_HASH_BITS)
struct file_hash_entry {
@@ -71,8 +126,8 @@ static unsigned int hash_str(const char *s)
return h & (FILE_HASH_SIZE - 1);
}
-static void add_entry(unsigned int offset, unsigned int file_id,
- unsigned int line)
+static void add_entry(unsigned int offset, unsigned int section_id,
+ unsigned int file_id, unsigned int line)
{
if (num_entries >= entries_capacity) {
entries_capacity = entries_capacity ? entries_capacity * 2 : 65536;
@@ -83,6 +138,7 @@ static void add_entry(unsigned int offset, unsigned int file_id,
}
}
entries[num_entries].offset = offset;
+ entries[num_entries].section_id = section_id;
entries[num_entries].file_id = file_id;
entries[num_entries].line = line;
num_entries++;
@@ -155,27 +211,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;
+ }
}
/*
@@ -201,9 +255,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)
@@ -211,6 +298,9 @@ static int compare_entries(const void *a, const void *b)
const struct line_entry *ea = a;
const struct line_entry *eb = b;
+ /* Group by section first so each per-section table is contiguous. */
+ if (ea->section_id != eb->section_id)
+ return ea->section_id < eb->section_id ? -1 : 1;
if (ea->offset != eb->offset)
return ea->offset < eb->offset ? -1 : 1;
if (ea->file_id != eb->file_id)
@@ -222,7 +312,8 @@ static int compare_entries(const void *a, const void *b)
/*
* Look up a vmlinux symbol by exact name and return its st_value, or
- * @fallback if absent. Aborts when @required and the symbol is missing.
+ * @fallback if the symbol is absent (lets callers gracefully skip
+ * optional bounds like _etext).
*/
static unsigned long long find_vmlinux_sym(Elf *elf, const char *name,
unsigned long long fallback,
@@ -270,22 +361,325 @@ static unsigned long long find_text_addr(Elf *elf)
}
/*
- * vmlinux is linked in multiple passes: gen_lineinfo runs against
- * .tmp_vmlinux1 (which carries an empty lineinfo stub), then real tables
- * are linked in for the final image. Sections placed AFTER .rodata
- * (.init.text, .exit.text, ...) shift forward as .rodata grows to hold
- * the real lineinfo blob, so DWARF addresses we'd capture for them in
- * pass 1 would be stale in the final kernel. Cap captured addresses at
- * _etext, the symbol that marks the end of .text — placed before .rodata
- * in every architecture's vmlinux.lds.S, so its addresses are invariant
- * across the relink. Returns 0 if _etext is absent (no cap; v3 behavior).
+ * Vmlinux is linked in multiple passes: gen_lineinfo runs against
+ * .tmp_vmlinux1 (which carries the empty lineinfo stub), and the resulting
+ * tables are then linked into the final vmlinux. Sections placed AFTER
+ * .rodata (.init.text, .exit.text, ...) shift forward as the real lineinfo
+ * tables replace the empty stub, so DWARF addresses we'd capture for them
+ * here are stale by the time the kernel runs.
+ *
+ * Cap the captured range at _etext, the symbol that marks the end of the
+ * .text section. .text is placed BEFORE .rodata in every architecture's
+ * vmlinux.lds.S, so its addresses are invariant across the relink.
+ * Returns 0 on architectures or builds that don't expose _etext, in which
+ * case the cap is disabled (preserving the v3 behavior — addresses past
+ * .text remain captured but may be off in stack traces).
*/
static unsigned long long find_text_end_addr(Elf *elf)
{
return find_vmlinux_sym(elf, "_etext", 0, false);
}
-static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
+/*
+ * Populate @sections[].present/sec_index/size/bias. Sections that don't
+ * exist stay marked absent. Biases are assigned in array order: each
+ * present section gets a base equal to the running total of preceding
+ * present sections' sizes, rounded up to 16 to keep ranges sparse. This
+ * guarantees [bias, bias+size) ranges are pairwise disjoint and fit in
+ * u32 as long as the sum of all covered text sizes is below 4 GiB.
+ */
+static void resolve_covered_sections(Elf *elf,
+ struct covered_section *sections,
+ unsigned int num_sections)
+{
+ Elf_Scn *scn = NULL;
+ GElf_Shdr shdr;
+ size_t shstrndx;
+ unsigned long long cursor = 0;
+
+ 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)
+ continue;
+ for (unsigned int i = 0; i < num_sections; i++) {
+ if (sections[i].present)
+ continue;
+ if (strcmp(name, sections[i].name))
+ continue;
+ if (shdr.sh_size > UINT_MAX) {
+ fprintf(stderr,
+ "lineinfo: section %s exceeds 4 GiB (size=%llu); skipping\n",
+ name,
+ (unsigned long long)shdr.sh_size);
+ break;
+ }
+ sections[i].sec_index = elf_ndxscn(scn);
+ sections[i].size = shdr.sh_size;
+ sections[i].present = true;
+ break;
+ }
+ }
+
+ /* Pack present sections into non-overlapping bias ranges. */
+ for (unsigned int i = 0; i < num_sections; i++) {
+ if (!sections[i].present)
+ continue;
+ sections[i].bias = cursor;
+ cursor += sections[i].size;
+ cursor = (cursor + 15) & ~15ULL; /* pad for separation */
+ }
+}
+
+/* Look up a covered_section by ELF section header index. */
+static struct covered_section *section_by_index(struct covered_section *sections,
+ unsigned int num_sections,
+ unsigned int sec_index)
+{
+ for (unsigned int i = 0; i < num_sections; i++) {
+ if (sections[i].present && sections[i].sec_index == sec_index)
+ return §ions[i];
+ }
+ return NULL;
+}
+
+/*
+ * 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;
+ }
+}
+
+/*
+ * Determine the relocation type for a 64-bit absolute reference
+ * on the given architecture. Returns 0 on 32-bit-only architectures
+ * (where DW_LNE_set_address fits in 32 bits and r_type_abs32 covers it).
+ */
+static unsigned int r_type_abs64(unsigned int e_machine)
+{
+ switch (e_machine) {
+ case EM_X86_64: return R_X86_64_64;
+ case EM_AARCH64: return R_AARCH64_ABS64;
+ case EM_RISCV: return R_RISCV_64;
+ case EM_S390: return R_390_64;
+ case EM_MIPS: return R_MIPS_64;
+ case EM_PPC64: return R_PPC64_ADDR64;
+ case EM_LOONGARCH: return R_LARCH_64;
+ case EM_PARISC: return R_PARISC_DIR64;
+ default: return 0;
+ }
+}
+
+/*
+ * Write a 4- or 8-byte unsigned integer in target byte order.
+ * Cross-builds (e.g. x86_64 host -> s390 module) need the patched
+ * .debug_line bytes laid out per the .ko's e_ident[EI_DATA], not the host's.
+ */
+static void elf_write_uint(unsigned char *dst, uint64_t value, size_t size,
+ bool little_endian)
+{
+ if (little_endian) {
+ for (size_t i = 0; i < size; i++)
+ dst[i] = (value >> (i * 8)) & 0xff;
+ } else {
+ for (size_t i = 0; i < size; i++)
+ dst[i] = (value >> ((size - 1 - i) * 8)) & 0xff;
+ }
+}
+
+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, abs64_type;
+ bool target_le;
+ 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);
+ abs64_type = r_type_abs64(ehdr.e_machine);
+ if (!abs32_type && !abs64_type)
+ return;
+ target_le = (ehdr.e_ident[EI_DATA] == ELFDATA2LSB);
+
+ 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;
+ bool is_abs64;
+
+ if (!gelf_getrela(rela_data, i, &rela))
+ continue;
+
+ r_type = GELF_R_TYPE(rela.r_info);
+ r_sym = GELF_R_SYM(rela.r_info);
+
+ /*
+ * Two reloc widths matter for .debug_line:
+ * abs32 - DW_FORM_line_strp file-table refs into .debug_line_str
+ * abs64 - DW_LNE_set_address arguments (sequence start PCs)
+ * Without both, libdw sees zeros and reports wrong filenames or
+ * collapses every sequence to address 0 (collision after dedup).
+ */
+ if (abs32_type && r_type == abs32_type) {
+ is_abs64 = false;
+ } else if (abs64_type && r_type == abs64_type) {
+ is_abs64 = true;
+ } else {
+ continue;
+ }
+
+ if (!gelf_getsym(sym_data, r_sym, &sym))
+ continue;
+
+ size_t width = is_abs64 ? 8 : 4;
+ uint64_t value = (uint64_t)(sym.st_value + rela.r_addend);
+
+ /*
+ * If the relocation targets one of our covered text sections,
+ * fold in that section's synthetic bias so the patched DWARF
+ * address lands in a unique numeric range. String-ref relocs
+ * (DW_FORM_line_strp into .debug_line_str) target a different
+ * section, so the symbol-based check correctly excludes them
+ * from biasing — for both abs64 (64-bit ELF) and abs32 (32-bit
+ * ELF, where DW_LNE_set_address is also 4 bytes wide).
+ */
+ if (module_mode) {
+ struct covered_section *cs;
+
+ cs = section_by_index(all_sections, ALL_SECTIONS,
+ sym.st_shndx);
+ if (cs)
+ value += cs->bias;
+ }
+
+ if (!is_abs64)
+ value &= 0xffffffffULL;
+
+ if (rela.r_offset + width <= dl_data->d_size)
+ elf_write_uint((unsigned char *)dl_data->d_buf +
+ rela.r_offset,
+ value, width, target_le);
+ }
+}
+
+/*
+ * Decide which covered_section a (biased) DWARF address belongs to.
+ * apply_debug_line_relocations() has already added the section's bias to
+ * each line-program PC, so [bias, bias+size) ranges are pairwise disjoint
+ * and a simple linear scan picks the right bucket. Returns the index
+ * within @sections, or @num_sections if @addr falls outside every
+ * present range (caller skips the entry).
+ */
+static unsigned int classify_address(struct covered_section *sections,
+ unsigned int num_sections,
+ unsigned long long addr,
+ unsigned long long *out_offset)
+{
+ for (unsigned int i = 0; i < num_sections; i++) {
+ if (!sections[i].present)
+ continue;
+ if (addr < sections[i].bias)
+ continue;
+ if (addr >= sections[i].bias + sections[i].size)
+ continue;
+ *out_offset = addr - sections[i].bias;
+ return i;
+ }
+ return num_sections;
+}
+
+static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr,
+ struct covered_section *sections,
+ unsigned int num_sections)
{
Dwarf_Off off = 0, next_off;
size_t hdr_size;
@@ -312,7 +706,8 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
Dwarf_Addr addr;
const char *src;
const char *rel;
- unsigned int file_id, loffset;
+ unsigned int file_id, loffset, sec_id;
+ unsigned long long sec_off;
int lineno;
if (!line)
@@ -329,56 +724,87 @@ static void process_dwarf(Dwarf *dwarf, unsigned long long text_addr)
if (!src)
continue;
- if (addr < text_addr)
- continue;
- /*
- * Skip addresses past _etext. Sections after .rodata
- * shift when the real lineinfo replaces the empty stub
- * during the multi-pass vmlinux link, so any address
- * we'd capture there would be stale by the time the
- * final kernel runs.
- */
- if (text_end_addr && addr >= text_end_addr)
- continue;
-
- {
- unsigned long long raw_offset = addr - text_addr;
+ if (module_mode) {
+ /*
+ * In ET_REL .ko files .text/.init.text/.exit.text
+ * all share sh_addr == 0; classify_address picks
+ * the right bucket from the explicit ranges we
+ * captured.
+ */
+ sec_id = classify_address(sections, num_sections,
+ addr, &sec_off);
+ if (sec_id == num_sections)
+ continue;
+ if (sec_off > UINT_MAX) {
+ skipped_overflow++;
+ continue;
+ }
+ loffset = (unsigned int)sec_off;
+ sections[sec_id].n_entries++;
+ } else {
+ unsigned long long raw_offset;
+ if (addr < text_addr)
+ continue;
+ /*
+ * Skip addresses past _etext. Sections after
+ * .rodata shift when the real lineinfo replaces
+ * the empty stub during the multi-pass vmlinux
+ * link, so any address we'd capture there would
+ * be stale by the time the final kernel runs.
+ */
+ if (text_end_addr && addr >= text_end_addr)
+ continue;
+ raw_offset = addr - text_addr;
if (raw_offset > UINT_MAX) {
skipped_overflow++;
continue;
}
loffset = (unsigned int)raw_offset;
+ sec_id = 0;
}
rel = make_relative(src, comp_dir);
file_id = find_or_add_file(rel);
- add_entry(loffset, file_id, (unsigned int)lineno);
+ add_entry(loffset, sec_id, file_id, (unsigned int)lineno);
}
next:
off = next_off;
}
}
-static void deduplicate(void)
+static void deduplicate(struct covered_section *sections,
+ unsigned int num_sections)
{
unsigned int i, j;
if (num_entries < 2)
return;
- /* Sort by offset, then file_id, then line for stability */
+ /*
+ * Sort by section_id, then offset, then file_id, line. This groups
+ * each section's entries contiguously so the per-section emit can
+ * iterate a simple range, and ensures the binary search invariant
+ * (offsets ascending) holds within each section.
+ */
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
+ * Remove duplicates. Reset on a section_id boundary: the same offset
+ * can legitimately appear in two different sections (they all start
+ * at sh_addr 0 in ET_REL), and the "same as previous kept entry"
+ * collapse is only meaningful inside one section's binary-search
+ * domain.
*/
j = 0;
for (i = 1; i < num_entries; i++) {
+ if (entries[i].section_id != entries[j].section_id) {
+ j++;
+ if (j != i)
+ entries[j] = entries[i];
+ continue;
+ }
if (entries[i].offset == entries[j].offset)
continue;
if (entries[i].file_id == entries[j].file_id &&
@@ -389,6 +815,14 @@ static void deduplicate(void)
entries[j] = entries[i];
}
num_entries = j + 1;
+
+ /* Recompute per-section n_entries from the deduped array. */
+ if (sections) {
+ for (unsigned int k = 0; k < num_sections; k++)
+ sections[k].n_entries = 0;
+ for (i = 0; i < num_entries; i++)
+ sections[entries[i].section_id].n_entries++;
+ }
}
static void compute_file_offsets(void)
@@ -486,6 +920,199 @@ static void output_assembly(void)
printf("\n");
}
+/*
+ * Emit one per-section table in the simple flat-array layout:
+ *
+ * mod_lineinfo_header
+ * addrs[count] (u32, sorted)
+ * file_ids[count] (u16) + 2-byte pad if count is odd
+ * lines[count] (u32)
+ * file_offsets[] (u32)
+ * filenames[]
+ *
+ * @suffix uniquifies labels so multiple tables can coexist in one blob.
+ * Caller has sorted entries[] so this section's entries occupy [first,
+ * first + count).
+ */
+static void emit_section_table(unsigned int first, unsigned int count,
+ const char *suffix)
+{
+ printf(".Lhdr%s:\n", suffix);
+ printf("\t.balign 4\n");
+ printf("\t.long %u\t\t/* num_entries */\n", count);
+ printf("\t.long %u\t\t/* num_files */\n", num_files);
+ printf("\t.long .Lfilenames_end%s - .Lfilenames%s\n\n", suffix, suffix);
+
+ /* addrs[] */
+ for (unsigned int i = 0; i < count; i++)
+ printf("\t.long 0x%x\n", entries[first + i].offset);
+
+ /* file_ids[] */
+ for (unsigned int i = 0; i < count; i++)
+ printf("\t.short %u\n", entries[first + i].file_id);
+ if (count & 1)
+ printf("\t.short 0\t\t/* pad to align lines[] */\n");
+
+ /* lines[] */
+ for (unsigned int i = 0; i < count; i++)
+ printf("\t.long %u\n", entries[first + i].line);
+
+ /* file_offsets[] */
+ printf("\t.balign 4\n");
+ for (unsigned int i = 0; i < num_files; i++)
+ printf("\t.long %u\n", files[i].str_offset);
+
+ /* filenames[] */
+ printf(".Lfilenames%s:\n", suffix);
+ for (unsigned int i = 0; i < num_files; i++)
+ print_escaped_asciz(files[i].name);
+ printf(".Lfilenames_end%s:\n", suffix);
+}
+
+/*
+ * Emit one mod_lineinfo_section descriptor. The "anchor" field is a
+ * relocation against the named ELF section symbol; the module loader
+ * resolves it on load to the runtime base of that section.
+ *
+ * On 64-bit ELF: 8-byte slot via .quad <name> (R_*_64 reloc).
+ * On 32-bit ELF: 4-byte reloc via .long <name>, plus 4 bytes of zero
+ * padding. The two halves are ordered to match target endianness so a
+ * naive u64 read on the kernel side recovers the relocated value.
+ */
+static void emit_section_descriptor(const char *section_name,
+ unsigned long long size,
+ const char *table_label,
+ const char *root_label)
+{
+ if (target_64bit) {
+ printf("\t.quad %s\t/* sections[].anchor (RELOC) */\n",
+ section_name);
+ } else if (target_le) {
+ printf("\t.long %s\t/* sections[].anchor low (RELOC) */\n",
+ section_name);
+ printf("\t.long 0\t\t/* sections[].anchor high pad */\n");
+ } else {
+ printf("\t.long 0\t\t/* sections[].anchor high pad */\n");
+ printf("\t.long %s\t/* sections[].anchor low (RELOC) */\n",
+ section_name);
+ }
+ printf("\t.long %llu\t/* sections[].size */\n", size);
+ printf("\t.long %s - %s\t/* sections[].table_offset */\n",
+ table_label, root_label);
+}
+
+/*
+ * Emit one .mod_lineinfo / .init.mod_lineinfo blob. Walks all_sections[]
+ * picking only entries that (a) belong to the requested blob and (b)
+ * actually produced at least one DWARF line entry — sections present in
+ * the .ko but without DWARF (e.g. compiler-generated stub thunks) are
+ * silently skipped. The caller-supplied entries[] is already sorted by
+ * section_id, so each section's entries are contiguous; we walk the
+ * master array in order to compute per-section starting indices.
+ */
+static void emit_blob(const char *output_section,
+ const char *blob_tag,
+ enum mod_lineinfo_blob blob)
+{
+ unsigned int active = 0;
+ unsigned int section_starts[ALL_SECTIONS];
+ unsigned int cursor = 0;
+
+ for (unsigned int i = 0; i < ALL_SECTIONS; i++) {
+ section_starts[i] = cursor;
+ cursor += all_sections[i].n_entries;
+ if (all_sections[i].blob == blob && all_sections[i].n_entries)
+ active++;
+ }
+
+ if (!active)
+ return;
+
+ printf("\t.section %s, \"a\"\n\n", output_section);
+
+ printf("\t.balign 8\n");
+ printf(".Lroot_%s:\n", blob_tag);
+ printf("\t.long %u\t\t/* num_sections */\n", active);
+ /* Pad to align the u64 anchor in sections[0] to 8 bytes. */
+ printf("\t.balign 8\n");
+
+ {
+ unsigned int slot = 0;
+ for (unsigned int i = 0; i < ALL_SECTIONS; i++) {
+ char table_label[64];
+ char root_label[64];
+
+ if (all_sections[i].blob != blob)
+ continue;
+ if (!all_sections[i].n_entries)
+ continue;
+ snprintf(table_label, sizeof(table_label),
+ ".Lhdr_%s_%u", blob_tag, slot);
+ snprintf(root_label, sizeof(root_label),
+ ".Lroot_%s", blob_tag);
+ emit_section_descriptor(all_sections[i].name,
+ all_sections[i].size,
+ table_label, root_label);
+ slot++;
+ }
+ }
+ printf("\n");
+
+ {
+ unsigned int slot = 0;
+
+ for (unsigned int i = 0; i < ALL_SECTIONS; i++) {
+ char suffix[64];
+
+ if (all_sections[i].blob != blob)
+ continue;
+ if (!all_sections[i].n_entries)
+ continue;
+ snprintf(suffix, sizeof(suffix), "_%s_%u",
+ blob_tag, slot);
+ emit_section_table(section_starts[i],
+ all_sections[i].n_entries,
+ suffix);
+ slot++;
+ }
+ }
+ printf("\n");
+}
+
+/*
+ * Declare each text-like section we plan to reference as an empty
+ * SHF_EXECINSTR section in this object. Without these stanzas the
+ * assembler treats `.quad .exit.text` as an undefined external symbol;
+ * after ld -r the resulting GLOBAL UND `.exit.text` doesn't bind to the
+ * .ko's LOCAL SECTION symbol of the same name, leaving depmod with an
+ * unresolved-symbol warning and the loader unable to relocate the anchor.
+ *
+ * Declaring the section here gives lineinfo.o its own local SECTION
+ * symbol; ld -r merges sections by name so the local symbol simply
+ * relocates to offset 0 of the merged section (lineinfo.o is linked
+ * FIRST so its zero-byte contribution stays at the start).
+ */
+static void declare_empty_text_sections(void)
+{
+ for (unsigned int i = 0; i < ALL_SECTIONS; i++) {
+ if (!all_sections[i].present)
+ continue;
+ printf("\t.section %s, \"ax\"\n", all_sections[i].name);
+ }
+ printf("\n");
+}
+
+static void output_module_assembly(void)
+{
+ 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");
+
+ declare_empty_text_sections();
+}
+
int main(int argc, char *argv[])
{
int fd;
@@ -493,12 +1120,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));
@@ -506,7 +1144,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()));
@@ -514,8 +1152,34 @@ int main(int argc, char *argv[])
return 1;
}
- text_addr = find_text_addr(elf);
- text_end_addr = find_text_end_addr(elf);
+ {
+ GElf_Ehdr ehdr;
+
+ if (gelf_getehdr(elf, &ehdr) == NULL) {
+ fprintf(stderr, "gelf_getehdr failed\n");
+ elf_end(elf);
+ close(fd);
+ return 1;
+ }
+ target_64bit = (ehdr.e_ident[EI_CLASS] == ELFCLASS64);
+ target_le = (ehdr.e_ident[EI_DATA] == ELFDATA2LSB);
+ }
+
+ if (module_mode) {
+ /*
+ * .ko files are ET_REL after ld -r. Resolve covered text
+ * sections FIRST so apply_debug_line_relocations() can use
+ * the assigned biases when patching line-program addresses;
+ * libdw does NOT apply relocations for ET_REL files, so we
+ * also handle DW_FORM_line_strp refs into .debug_line_str.
+ */
+ resolve_covered_sections(elf, all_sections, ALL_SECTIONS);
+ apply_debug_line_relocations(elf);
+ text_addr = 0; /* unused in module mode */
+ } else {
+ text_addr = find_text_addr(elf);
+ text_end_addr = find_text_end_addr(elf);
+ }
dwarf = dwarf_begin_elf(elf, DWARF_C_READ, NULL);
if (!dwarf) {
@@ -528,20 +1192,55 @@ int main(int argc, char *argv[])
return 1;
}
- process_dwarf(dwarf, text_addr);
+ if (module_mode) {
+ unsigned int persistent_total, init_total;
+
+ output_module_assembly(); /* file header only */
- if (skipped_overflow)
+ /*
+ * Single DWARF pass classifies every line entry into its
+ * covering section (or skips it). Each entry is tagged with
+ * the master-array section_id so per-blob emit can filter.
+ */
+ process_dwarf(dwarf, 0, all_sections, ALL_SECTIONS);
+ deduplicate(all_sections, ALL_SECTIONS);
+ compute_file_offsets();
+
+ emit_blob(".mod_lineinfo", "p", BLOB_PERSISTENT);
+ emit_blob(".init.mod_lineinfo", "i", BLOB_INIT);
+
+ persistent_total = 0;
+ init_total = 0;
+ for (unsigned int i = 0; i < ALL_SECTIONS; i++) {
+ if (all_sections[i].blob == BLOB_PERSISTENT)
+ persistent_total += all_sections[i].n_entries;
+ else if (all_sections[i].blob == BLOB_INIT)
+ init_total += all_sections[i].n_entries;
+ }
fprintf(stderr,
- "lineinfo: warning: %u entries skipped (offset > 4 GiB from _text)\n",
- skipped_overflow);
+ "lineinfo: persistent %u entries, init %u entries, %u files\n",
+ persistent_total, init_total, num_files);
+
+ if (skipped_overflow)
+ fprintf(stderr,
+ "lineinfo: warning: %u entries skipped (offset > 4 GiB)\n",
+ skipped_overflow);
+ } else {
+ process_dwarf(dwarf, text_addr, NULL, 0);
- deduplicate();
- compute_file_offsets();
+ if (skipped_overflow)
+ fprintf(stderr,
+ "lineinfo: warning: %u entries skipped (offset > 4 GiB from _text)\n",
+ skipped_overflow);
- fprintf(stderr, "lineinfo: %u entries, %u files\n",
- num_entries, num_files);
+ deduplicate(NULL, 0);
+ compute_file_offsets();
- output_assembly();
+ fprintf(stderr, "lineinfo: %u entries, %u files\n",
+ num_entries, num_files);
+
+ output_assembly();
+ }
dwarf_end(dwarf);
elf_end(elf);
@@ -552,6 +1251,5 @@ int main(int argc, char *argv[])
for (unsigned int i = 0; i < num_files; i++)
free(files[i].name);
free(files);
-
return 0;
}
--
2.53.0
^ permalink raw reply related
* [PATCH v5 1/4] kallsyms: embed source file:line info in kernel stack traces
From: Sasha Levin @ 2026-05-04 15:33 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, Zhen Lei,
Sami Tolvanen, linux-kernel, linux-kbuild, linux-modules,
linux-doc, Sasha Levin
In-Reply-To: <20260504153401.2416391-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%)
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 | 76 ++-
kernel/kallsyms_internal.h | 9 +
scripts/.gitignore | 1 +
scripts/Makefile | 3 +
scripts/empty_lineinfo.S | 30 +
scripts/gen_lineinfo.c | 557 ++++++++++++++++++
scripts/kallsyms.c | 16 +
scripts/link-vmlinux.sh | 43 +-
13 files changed, 841 insertions(+), 10 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 cd28dfe91b060..37456e08fe43c 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 2fb1c75afd163..a683a7023f6e9 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13793,6 +13793,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 2937c4d308aec..99e78c253056a 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2070,6 +2070,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..1e3f527b13988 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -467,9 +467,56 @@ 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)
+ int symbol_offset, int add_offset, int add_buildid,
+ int add_lineinfo)
{
char *modname;
const unsigned char *buildid;
@@ -497,6 +544,23 @@ static int __sprint_symbol(char *buffer, unsigned long address,
len += sprintf(buffer + len, "]");
}
+ /*
+ * Append "(file:line)" only for stack-backtrace consumers. Plain
+ * sprint_symbol() backs %ps, and many existing format strings tack
+ * literal "()" after %ps to indicate a function call ("foo()
+ * replaced with bar()"); appending lineinfo there would produce a
+ * confusing "foo (file:line)()".
+ */
+ if (add_lineinfo && 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;
}
@@ -513,7 +577,7 @@ static int __sprint_symbol(char *buffer, unsigned long address,
*/
int sprint_symbol(char *buffer, unsigned long address)
{
- return __sprint_symbol(buffer, address, 0, 1, 0);
+ return __sprint_symbol(buffer, address, 0, 1, 0, 0);
}
EXPORT_SYMBOL_GPL(sprint_symbol);
@@ -530,7 +594,7 @@ EXPORT_SYMBOL_GPL(sprint_symbol);
*/
int sprint_symbol_build_id(char *buffer, unsigned long address)
{
- return __sprint_symbol(buffer, address, 0, 1, 1);
+ return __sprint_symbol(buffer, address, 0, 1, 1, 0);
}
EXPORT_SYMBOL_GPL(sprint_symbol_build_id);
@@ -547,7 +611,7 @@ EXPORT_SYMBOL_GPL(sprint_symbol_build_id);
*/
int sprint_symbol_no_offset(char *buffer, unsigned long address)
{
- return __sprint_symbol(buffer, address, 0, 0, 0);
+ return __sprint_symbol(buffer, address, 0, 0, 0, 0);
}
EXPORT_SYMBOL_GPL(sprint_symbol_no_offset);
@@ -567,7 +631,7 @@ EXPORT_SYMBOL_GPL(sprint_symbol_no_offset);
*/
int sprint_backtrace(char *buffer, unsigned long address)
{
- return __sprint_symbol(buffer, address, -1, 1, 0);
+ return __sprint_symbol(buffer, address, -1, 1, 0, 1);
}
/**
@@ -587,7 +651,7 @@ int sprint_backtrace(char *buffer, unsigned long address)
*/
int sprint_backtrace_build_id(char *buffer, unsigned long address)
{
- return __sprint_symbol(buffer, address, -1, 1, 1);
+ return __sprint_symbol(buffer, address, -1, 1, 1, 1);
}
/* To avoid using get_symbol_offset for every symbol, we carry prefix along. */
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 3434a82a119f0..55244ce955781 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
@@ -37,6 +38,8 @@ HOSTCFLAGS_asn1_compiler.o = -I$(srctree)/include
HOSTCFLAGS_sign-file.o = $(shell $(HOSTPKG_CONFIG) --cflags libcrypto 2> /dev/null)
HOSTCFLAGS_sign-file.o += -I$(srctree)/tools/include/uapi/
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..699e760178f09
--- /dev/null
+++ b/scripts/gen_lineinfo.c
@@ -0,0 +1,557 @@
+// 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 <stdbool.h>
+#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;
+
+/*
+ * vmlinux mode: end of the invariant .text region. Zero means "no cap"
+ * (graceful fallback when _etext is absent on some build).
+ */
+static unsigned long long text_end_addr;
+
+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;
+}
+
+/*
+ * Look up a vmlinux symbol by exact name and return its st_value, or
+ * @fallback if absent. Aborts when @required and the symbol is missing.
+ */
+static unsigned long long find_vmlinux_sym(Elf *elf, const char *name,
+ unsigned long long fallback,
+ bool required)
+{
+ 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 *sname;
+
+ if (!gelf_getsym(data, i, &sym))
+ continue;
+ sname = elf_strptr(elf, shdr.sh_link, sym.st_name);
+ if (sname && !strcmp(sname, name))
+ return sym.st_value;
+ }
+ }
+
+ if (required) {
+ fprintf(stderr, "Cannot find %s symbol\n", name);
+ exit(1);
+ }
+ return fallback;
+}
+
+static unsigned long long find_text_addr(Elf *elf)
+{
+ return find_vmlinux_sym(elf, "_text", 0, true);
+}
+
+/*
+ * vmlinux is linked in multiple passes: gen_lineinfo runs against
+ * .tmp_vmlinux1 (which carries an empty lineinfo stub), then real tables
+ * are linked in for the final image. Sections placed AFTER .rodata
+ * (.init.text, .exit.text, ...) shift forward as .rodata grows to hold
+ * the real lineinfo blob, so DWARF addresses we'd capture for them in
+ * pass 1 would be stale in the final kernel. Cap captured addresses at
+ * _etext, the symbol that marks the end of .text — placed before .rodata
+ * in every architecture's vmlinux.lds.S, so its addresses are invariant
+ * across the relink. Returns 0 if _etext is absent (no cap; v3 behavior).
+ */
+static unsigned long long find_text_end_addr(Elf *elf)
+{
+ return find_vmlinux_sym(elf, "_etext", 0, false);
+}
+
+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;
+ /*
+ * Skip addresses past _etext. Sections after .rodata
+ * shift when the real lineinfo replaces the empty stub
+ * during the multi-pass vmlinux link, so any address
+ * we'd capture there would be stale by the time the
+ * final kernel runs.
+ */
+ if (text_end_addr && addr >= text_end_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);
+ text_end_addr = find_text_end_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.53.0
^ permalink raw reply related
* [PATCH v5 0/4] kallsyms: embed source file:line info in kernel stack traces
From: Sasha Levin @ 2026-05-04 15:33 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, Zhen Lei,
Sami Tolvanen, 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_backtrace() 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 .mod_lineinfo and
.init.mod_lineinfo sections embedded at build time, each carrying one
or more per-section sub-tables identified by an ELF relocation against
the covered text section's symbol. The module loader's existing
apply_relocations() pass resolves the anchor on load - no special-cased
loader logic, no implicit MOD_TEXT-base assumption. Coverage extends
to .text, .exit.text, and .init.text; the init blob is freed alongside
init memory.
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. Compression applies independently
to each per-section table inside the module blobs.
Patch 4: KUnit tests
30 KUnit tests covering the lineinfo lookup paths, delta-decode logic,
boundary conditions, the module-side multi-section format, 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)
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 v4
================
- Replace mod->mem[MOD_TEXT].base derivation with an ELF relocation
against the covered section's symbol; resolved by the module loader
on load. (Suggested by Petr Pavlu)
- Cover .exit.text and .init.text in addition to .text. Init code
lives in a parallel .init.mod_lineinfo blob (MOD_INIT_RODATA) that
is revoked and freed alongside init memory.
- Switch the build step from objcopy --add-section to ld -r, so
.rela.mod_lineinfo / .rela.init.mod_lineinfo ride along into the .ko.
- Pick up lineinfo sections via section_objs() (not any_section_objs)
to validate SHF_ALLOC and avoid dangling pointers into the temporary
load image.
- Drop the unused "reserved" placeholder field from mod_lineinfo_root
and mod_lineinfo_header. (Suggested by Petr Pavlu)
- Guard the per-module fields with #ifdef CONFIG_KALLSYMS_LINEINFO_MODULES
and expose them via inline reader accessors. (Suggested by Petr Pavlu)
- Update the per-section header layout comment to name struct fields.
(Suggested by Petr Pavlu)
- Cap vmlinux-side captured DWARF addresses at _etext: sections placed
after .rodata shift as the real lineinfo replaces the empty stub
during the multi-pass link, so addresses outside the invariant .text
window become stale in the final image.
- Append "(file:line)" only via sprint_backtrace*(), not plain
sprint_symbol*() (which backs %ps); many format strings already
follow %ps with literal "()" and would otherwise render as
"foo (file:line)()". KUnit gains a sprint_with_lineinfo() helper.
- Build-side fixes in scripts/gen_lineinfo: apply 64-bit absolute
relocations to .debug_line (libdw doesn't relocate ET_REL inputs);
endian-safe writes for cross-builds; declare empty
.text/.exit.text/.init.text stanzas so ld -r binds anchors to LOCAL
rather than GLOBAL UND symbols.
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.
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 | 114 +-
include/linux/kallsyms.h | 17 +-
include/linux/mod_lineinfo.h | 280 ++++
include/linux/module.h | 39 +
init/Kconfig | 35 +
kernel/kallsyms.c | 78 +-
kernel/kallsyms_internal.h | 11 +
kernel/module/kallsyms.c | 171 +++
kernel/module/main.c | 26 +
lib/Kconfig.debug | 10 +
lib/tests/Makefile | 3 +
lib/tests/lineinfo_kunit.c | 870 +++++++++++
scripts/.gitignore | 1 +
scripts/Makefile | 3 +
scripts/Makefile.modfinal | 6 +
scripts/empty_lineinfo.S | 38 +
scripts/gen-mod-lineinfo.sh | 50 +
scripts/gen_lineinfo.c | 1322 +++++++++++++++++
scripts/kallsyms.c | 17 +
scripts/link-vmlinux.sh | 43 +-
22 files changed, 3187 insertions(+), 45 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 100644 scripts/gen-mod-lineinfo.sh
create mode 100644 scripts/gen_lineinfo.c
--
2.53.0
^ permalink raw reply
* Re: [PATCH V10 00/10] famfs: port into fuse
From: Bernd Schubert @ 2026-05-04 15:08 UTC (permalink / raw)
To: John Groves, John Groves, Miklos Szeredi, Dan Williams,
Alison Schofield
Cc: John Groves, Jonathan Corbet, Shuah Khan, Vishal Verma,
Dave Jiang, Matthew Wilcox, Jan Kara, Alexander Viro,
David Hildenbrand, Christian Brauner, Darrick J . Wong,
Randy Dunlap, Jeff Layton, Amir Goldstein, Jonathan Cameron,
Stefan Hajnoczi, Joanne Koong, Josef Bacik, Bagas Sanjaya,
Chen Linxuan, James Morse, Fuad Tabba, Sean Christopherson,
Shivank Garg, Ackerley Tng, Gregory Price, Aravind Ramesh,
Ajay Joshi, venkataravis@micron.com, linux-doc@vger.kernel.org,
linux-kernel@vger.kernel.org, nvdimm@lists.linux.dev,
linux-cxl@vger.kernel.org, linux-fsdevel@vger.kernel.org
In-Reply-To: <0100019d43e5f632-f5862a3e-361c-4b54-a9a6-96c242a8f17a-000000@email.amazonses.com>
[-- Attachment #1: Type: text/plain, Size: 336 bytes --]
Hi John,
I started to do reviews based on design documents for new features. I
think in general it is rather hard for humans to kind of reverse
engineer the design from the patch series. With AI it got easier, but
still should be verified by the author. Could you check if the attached
AI generated document is correct?
Thanks,
Bernd
[-- Attachment #2: famfs-design.txt --]
[-- Type: text/plain, Size: 37134 bytes --]
================================================================================
famfs (FUSE-based fabric-attached memory file system) - Design Document
================================================================================
Audience and scope
==================
This document is written for people already familiar with FUSE (lowlevel ops,
opcodes, INIT capability negotiation) but NOT necessarily with Linux DAX,
devdax, or the kernel's iomap framework. Section 2 is a primer on those.
It covers two trees:
Kernel: /home/bernd/src/linux/linux.git , branch `famfs`,
commits 4a8ae428c392 .. HEAD (da9edf77cbc4)
libfuse: /home/bernd/src/libfuse/libfuse.git , branch `famfs`,
commits d75ae2ee .. HEAD (9c65d781)
Kernel files added or changed:
fs/fuse/famfs.c - new, all famfs kernel logic
fs/fuse/famfs_kfmap.h - new, in-memory fmap structures
fs/fuse/fuse_i.h - per-inode/per-conn famfs additions, helpers
fs/fuse/file.c - r/w/mmap dispatch into famfs paths
fs/fuse/inode.c - INIT-flag negotiation, conn teardown wiring
fs/fuse/iomode.c - bypass io-modes for famfs files
fs/fuse/Kconfig, Makefile - new CONFIG_FUSE_FAMFS_DAX
include/uapi/linux/fuse.h - new opcodes, structs, FUSE_DAX_FMAP flag
fs/namei.c - export may_open_dev()
Documentation/filesystems/famfs.rst - user/admin documentation
libfuse files added or changed:
include/fuse_kernel.h - mirror of kernel uapi at protocol 7.46
include/fuse_common.h - new FUSE_CAP_DAX_FMAP capability bit
include/fuse_lowlevel.h - new ops: get_fmap(), get_daxdev()
lib/fuse_lowlevel.c - INIT negotiation + opcode dispatch
(do_get_fmap, do_get_daxdev)
--------------------------------------------------------------------------------
1. Background and goals
--------------------------------------------------------------------------------
Famfs exposes shared, fabric-attached memory (CXL devdax) as a regular
filesystem. The fast path (read/write/mmap-fault) must reach memory without a
round trip to the FUSE server: the server only delivers metadata.
Two key observations shape the design:
* Files are NEVER allocated in the kernel. Userspace pre-allocates extents
and gives the kernel an "fmap" (file-to-dax-offset map).
* There is NO writeback. Backing memory is the storage; CPU caches are
loaded directly from the dax memory.
Consequences in the kernel:
* No page cache is used. `noop_dirty_folio` is the only address_space op.
* The kernel never grows or shrinks files. Any size change (including
truncate) puts the file into an "error" state.
* Reads/writes/mmap dispatch through `dax_iomap_*()` and the famfs
`iomap_ops`, exactly the way fs-dax filesystems (xfs/ext4) plumb them.
Comparison to other FUSE modes that you may know:
classic FUSE - every read/write/mmap is forwarded to the server.
virtio-fs DAX - the server donates a window of host memory; kernel maps
file ranges into that window via FUSE_SETUPMAPPING /
FUSE_REMOVEMAPPING. The server is still the "owner" of
the backing memory.
famfs (this) - the server hands the kernel a description of where each
file's bytes live on a real character device (devdax).
After that, the server is OUT of the data path entirely.
--------------------------------------------------------------------------------
2. Primer: devdax, DAX and iomap (only what's needed below)
--------------------------------------------------------------------------------
You can skip this section if "iomap_begin", "dax_iomap_rw" and "devdax holder"
already mean something to you.
devdax
A character device (`/dev/daxN.M`) that exposes a contiguous range of
physical memory directly to userspace via mmap. There is no page cache
and no block device underneath; reads and writes hit RAM/CXL memory
directly. Famfs uses devdax devices as its "disks".
DAX (Direct Access)
A kernel pathway that lets a filesystem map file pages straight onto
the underlying memory pages (PFNs) without going through the page cache.
A file/inode tagged with `S_DAX` opts in. Reads turn into memcpy from
the memory; mmap faults install the memory's PFN directly into the page
table (PTE/PMD/PUD).
iomap
A filesystem-agnostic mechanism that says "to do this read/write/fault
on this file at this offset and length, here is exactly which device,
which device-relative offset, and how many bytes are valid here."
Filesystems implement `struct iomap_ops`, of which the central callback
is:
.iomap_begin(inode, file_offset, length, flags,
struct iomap *out, struct iomap *srcmap)
The filesystem fills `out` with:
out->dax_dev - which DAX device backs this range
out->addr - byte offset within that DAX device
out->offset - file offset (echoed back)
out->length - how many contiguous bytes are valid here
out->type - IOMAP_MAPPED (famfs only ever returns this)
The DAX core then loops, calling `iomap_begin` repeatedly to walk the
requested range and, for each chunk, doing either:
- memcpy to/from `dax_dev + addr` (read/write)
- or installing the PFN at `dax_dev + addr` into a page table (faults)
Entry points the famfs code uses:
dax_iomap_rw(iocb, iter, ops) - read/write
dax_iomap_fault(vmf, order, ..., ops) - mmap PTE/PMD/PUD fault
dax holder
DAX devices have a single "holder" - a struct (here `struct fuse_conn *`)
that owns the device. Acquired via `fs_dax_get(devp, holder, holder_ops)`,
released via `fs_put_dax(devp, holder)`. The holder gets called back via
`holder_ops->notify_failure()` when the device reports memory poison.
That is the entire iomap-related vocabulary used in this document.
--------------------------------------------------------------------------------
3. Major kernel data structures
--------------------------------------------------------------------------------
(a) fuse_conn additions (fs/fuse/fuse_i.h):
struct fuse_conn {
...
unsigned int famfs_iomap : 1; /* negotiated at INIT */
struct rw_semaphore famfs_devlist_sem; /* protects dax_devlist */
struct famfs_dax_devlist *dax_devlist; /* table of daxdevs */
};
(b) fuse_inode additions:
struct fuse_inode {
...
void *famfs_meta; /* struct famfs_file_meta *, NULL if not famfs */
};
A non-NULL `famfs_meta` is the marker for "this is a famfs file";
`fuse_file_famfs(fi)` is just `READ_ONCE(fi->famfs_meta) != NULL`.
(c) Per-file metadata - struct famfs_file_meta (famfs_kfmap.h):
+------------------------------------------------------+
| struct famfs_file_meta |
| bool error |
| enum famfs_file_type file_type |
| size_t file_size |
| enum famfs_extent_type fm_extent_type |
| u64 dev_bitmap |
| union { |
| SIMPLE: |
| size_t fm_nextents |
| struct famfs_meta_simple_ext *se |
| INTERLEAVED: |
| size_t fm_niext |
| struct famfs_meta_interleaved_ext *ie |
| } |
+------------------------------------------------------+
Simple extent: (dev_index, ext_offset, ext_len)
Interleaved extent: (nstrips, chunk_size, nbytes, strips[])
where each strip is a simple extent.
(d) Per-conn dax device table:
+-------------------------+ +------------------------------+
| famfs_dax_devlist | | famfs_daxdev[MAX_DAXDEVS=24] |
| nslots = MAX_DAXDEVS |----->| |
| ndevs | | [0] valid? devp, devno, ... |
| devlist *-------------| | [1] valid? devp, devno, ... |
+-------------------------+ | ... |
+------------------------------+
famfs_daxdev fields:
valid - slot has been populated (after wmb)
error - dax notify_failure() arrived (poison)
dax_err - fs_dax_get() failed; cannot be used
devno, devp - dev_t and dax_device pointer
name - chrdev pathname for diagnostics
--------------------------------------------------------------------------------
4. Capability negotiation (FUSE INIT)
--------------------------------------------------------------------------------
The wire-level capability is `FUSE_DAX_FMAP` (bit 43 in the 64-bit flags
field, protocol 7.46). Both ends must advertise it in INIT for the kernel
to enable famfs.
Kernel FUSE server (libfuse)
------ ---------------------
fuse_new_init():
flags |= FUSE_DAX_FMAP -- if capable(CAP_SYS_RAWIO)
----------- FUSE_INIT (in) --------->
libfuse _do_init():
if (inargflags &
FUSE_DAX_FMAP)
conn.capable_ext |=
FUSE_CAP_DAX_FMAP
server's init_done CB
sets:
conn.want_ext |=
FUSE_CAP_DAX_FMAP
libfuse converts that
back to FUSE_DAX_FMAP
in outargflags
<----------- FUSE_INIT (out) ----------
process_init_reply():
if reply.flags & FUSE_DAX_FMAP &&
in.flags also had FUSE_DAX_FMAP:
famfs_init_devlist_sem(fc)
fc->famfs_iomap = 1
Both directions must agree. The kernel re-checks the flag in `in.flags` on the
reply path because process_init_reply() does not run in the server's task
context, so capable() cannot be re-evaluated then; the bit on the way OUT
asserts "the user that mounted us had CAP_SYS_RAWIO".
Kernel file: fs/fuse/inode.c (fuse_new_init, process_init_reply)
libfuse file: lib/fuse_lowlevel.c (_do_init)
--------------------------------------------------------------------------------
5. libfuse server-side surface
--------------------------------------------------------------------------------
This is the userspace API a famfs server is expected to implement on top of
libfuse's lowlevel API. From a FUSE-developer point of view this is the
familiar pattern: two new opcodes, two new callbacks in
`struct fuse_lowlevel_ops`, and a new capability bit.
5.1 Capability bit (include/fuse_common.h)
#define FUSE_CAP_DAX_FMAP (1UL << 32)
This sits in the *extended* capability fields `want_ext` / `capable_ext`,
not the legacy 32-bit `want` / `capable`, because bit 32 is past the end
of the original word.
5.2 New lowlevel callbacks (include/fuse_lowlevel.h)
struct fuse_lowlevel_ops {
...
/* Reply: serialized fuse_famfs_fmap_header followed by extents */
void (*get_fmap) (fuse_req_t req, fuse_ino_t ino, size_t size);
/* Reply: serialized fuse_daxdev_out (mainly: char name[256]) */
void (*get_daxdev) (fuse_req_t req, int daxdev_index);
};
Conventional libfuse semantics apply:
- The callback may reply asynchronously.
- Valid completions: fuse_reply_buf() with the serialized response,
or fuse_reply_err(req, errno) on failure.
- If the server does not provide either op, libfuse replies with
-EOPNOTSUPP automatically.
5.3 Opcode dispatch (lib/fuse_lowlevel.c)
Two entries are added to libfuse's opcode dispatch table:
[FUSE_GET_FMAP] = { do_get_fmap, "GET_FMAP" },
[FUSE_GET_DAXDEV] = { do_get_daxdev, "GET_DAXDEV" },
do_get_fmap:
reads `inarg` as `struct fuse_getxattr_in`*, extracts `arg->size`
(the kernel's hint for the maximum reply size it can accept),
forwards (req, ino, size) to op.get_fmap. The size is currently
fixed at PAGE_SIZE on the kernel side (FMAP_BUFSIZE); a larger
variable-size reply protocol is a future TODO.
do_get_daxdev:
ignores `inarg`. The kernel encodes the device index in `nodeid`
(FUSE_GET_DAXDEV uses nodeid as a small integer, not a real inode),
and libfuse forwards it as `daxdev_index` to op.get_daxdev.
5.4 Wire formats the server must produce
Defined in include/fuse_kernel.h (libfuse's mirror of the kernel uapi):
struct fuse_famfs_fmap_header {
uint8_t file_type; /* enum fuse_famfs_file_type */
uint8_t reserved;
uint16_t fmap_version; /* FAMFS_FMAP_VERSION = 1 */
uint32_t ext_type; /* SIMPLE or INTERLEAVE */
uint32_t nextents;
uint32_t reserved0;
uint64_t file_size;
uint64_t reserved1;
};
struct fuse_famfs_simple_ext {
uint32_t se_devindex; /* index into the per-mount daxdev table */
uint32_t reserved;
uint64_t se_offset; /* PMD-aligned offset in that daxdev */
uint64_t se_len; /* PMD-aligned length */
};
struct fuse_famfs_iext { /* one interleaved extent */
uint32_t ie_nstrips;
uint32_t ie_chunk_size; /* PMD-aligned */
uint64_t ie_nbytes; /* total bytes covered by this extent */
uint64_t reserved;
};
struct fuse_daxdev_out {
uint16_t index;
uint16_t reserved;
uint32_t reserved2;
uint64_t reserved3;
uint64_t reserved4;
char name[256]; /* "/dev/daxN.M" */
};
GET_FMAP reply layout in the buffer (fmap_header followed by extents):
SIMPLE: [ fmap_header ][ simple_ext * nextents ]
INTERLEAVE: [ fmap_header ][ iext, simple_ext*nstrips,
iext, simple_ext*nstrips, ... ]
where there are `nextents` (iext + its strips) groups.
Alignment rules the server MUST honor (else the kernel rejects the fmap):
* fmap_version == 1
* 1 <= nextents <= FUSE_FAMFS_MAX_EXTENTS (32)
* For each strip extent: ext_offset and ext_len PMD-aligned (2 MiB)
* For interleaved: chunk_size PMD-aligned, nstrips in [1, 32]
* sum of extent lengths >= file_size
GET_DAXDEV reply: a single fuse_daxdev_out where `name` is the path of a
character device that the kernel can `kern_path()` to a devdax inode.
5.5 What the server is responsible for
In the famfs design, the libfuse-based server still owns:
* Looking up files in the famfs metadata log (or whatever backend
userspace uses to track allocations).
* Producing fmaps that exactly describe the file's allocation.
* Producing the "/dev/daxN.M" path for each daxdev index it has
used in any fmap.
* All conventional FUSE ops: lookup, getattr, mkdir, unlink, etc.
The server is NOT in the path of any read/write/mmap once the fmap has
been delivered. There is no equivalent of FUSE_READ / FUSE_WRITE traffic
for famfs files.
--------------------------------------------------------------------------------
6. Open flow - GET_FMAP and (lazy) GET_DAXDEV
--------------------------------------------------------------------------------
When a regular file is opened on a famfs-enabled connection, the kernel pulls
the file's fmap from the server, parses it, resolves any unknown daxdev
indices via GET_DAXDEV, and installs the result on the inode.
fuse_open(inode, file) [fs/fuse/file.c]
|
+-- fuse_do_open() (regular FUSE open)
|
+-- if (fc->famfs_iomap && S_ISREG)
| fuse_get_fmap(fm, inode) [famfs.c]
| |
| +-- alloc fmap_buf (PAGE_SIZE)
| |
| +-- args.opcode = FUSE_GET_FMAP
| | args.nodeid = ino
| | args.out_argvar = true (variable-size reply)
| +-- fuse_simple_request(fm, &args) ----> server returns
| | fuse_famfs_fmap_header
| | + extents
| +-- famfs_file_init_dax(fm, inode, fmap_buf, fmap_size)
| |
| +-- famfs_fuse_meta_alloc()
| | parses header + extents into struct famfs_file_meta;
| | accumulates meta->dev_bitmap of referenced devindices;
| | validates PMD alignment + total size >= file_size;
| | cmpxchg-installs *metap (race-safe)
| |
| +-- famfs_update_daxdev_table(fm, meta)
| | if (!fc->dax_devlist) cmpxchg-allocate it
| | under famfs_devlist_sem (read):
| | collect indices that are NOT yet ->valid
| | drop lock, then for each index:
| | famfs_fuse_get_daxdev(fm, idx) <see below>
| |
| +-- inode_lock(inode)
| | famfs_meta_set(fi, meta) (cmpxchg, NULL=>meta)
| | if installed: i_size_write, S_DAX, a_ops=famfs_dax_aops
| | inode_unlock(inode)
|
+-- fuse_finish_open(inode, file)
+-- skip page cache invalidation if fuse_file_famfs(fi)
GET_DAXDEV per-index flow:
famfs_fuse_get_daxdev(fm, index):
args.opcode = FUSE_GET_DAXDEV
args.nodeid = index
fuse_simple_request() -----> server returns fuse_daxdev_out{.name = "/dev/daxX.Y"}
under famfs_devlist_sem (write):
if dd->valid: return /* lost race; OK */
famfs_verify_daxdev(name, &dd->devno):
kern_path() + d_backing_inode() + S_ISCHR
may_open_dev() /* exported in fs/namei.c */
dd->devno = inode->i_rdev
dd->name = kstrdup(name)
dd->devp = dax_dev_get(devno)
fs_dax_get(devp, fc, &famfs_fuse_dax_holder_ops)
on failure: dd->dax_err = 1 /* still mark valid */
wmb()
dd->valid = 1
--------------------------------------------------------------------------------
7. iomap interaction - the central design point
--------------------------------------------------------------------------------
famfs implements only `.iomap_begin`. There is no `.iomap_end` because there
is no allocation, dirty tracking, or completion bookkeeping.
const struct iomap_ops famfs_iomap_ops = {
.iomap_begin = famfs_fuse_iomap_begin,
};
The dax core (fs/dax.c) calls into famfs_iomap_ops from three entry points:
dax_iomap_rw(iocb, iter, ops) -- read_iter / write_iter
dax_iomap_fault(vmf, order, ...) -- mmap PTE/PMD/PUD faults
For the iomap concepts used here, see the primer in section 2.
7.1 read/write path
fuse_file_read_iter(iocb, to) [fs/fuse/file.c]
if (fuse_file_famfs(fi))
return famfs_fuse_read_iter(iocb, to); [famfs.c]
famfs_fuse_read_iter:
famfs_fuse_rw_prep(iocb, to):
famfs_file_bad(inode)? -> -EIO/-ENXIO
truncate iter to (i_size - ki_pos)
dax_iomap_rw(iocb, to, &famfs_iomap_ops) ===>
+-- repeatedly:
| iomap_begin(...)
| memcpy_from_pmem/to
| advance position
+-- returns bytes copied
famfs_fuse_write_iter is symmetric (no FOPEN_DIRECT_IO / passthrough fork;
splice paths return -EIO since famfs has no page cache).
7.2 mmap path
fuse_file_mmap(file, vma) [fs/fuse/file.c]
if (fuse_file_famfs(fi))
return famfs_fuse_mmap(file, vma);
famfs_fuse_mmap:
famfs_file_bad(inode)
vma->vm_ops = &famfs_file_vm_ops
vm_flags_set(vma, VM_HUGEPAGE) /* prefer 2MiB faults */
famfs_file_vm_ops:
.fault = famfs_filemap_fault (PTE)
.huge_fault = famfs_filemap_huge_fault (PMD/PUD)
.map_pages = filemap_map_pages
.page_mkwrite = famfs_filemap_mkwrite
.pfn_mkwrite = famfs_filemap_mkwrite
7.3 fault handler dispatch
__famfs_fuse_filemap_fault(vmf, pe_size, write_fault):
if (!IS_DAX(inode)) return SIGBUS
if (write_fault) sb_start_pagefault, file_update_time
ret = dax_iomap_fault(vmf, pe_size, &pfn, NULL, &famfs_iomap_ops)
|
+-- internally calls famfs_fuse_iomap_begin to learn (dax_dev,
offset, length), then maps the resolved PFN into the VMA.
if (ret & VM_FAULT_NEEDDSYNC) ret = dax_finish_sync_fault(...)
7.4 iomap_begin - the resolver
famfs_fuse_iomap_begin(inode, offset, length, flags, iomap, srcmap)
meta = fi->famfs_meta
WARN_ON(i_size != meta->file_size)
return famfs_fileofs_to_daxofs(inode, iomap, offset, length, flags)
famfs_fileofs_to_daxofs (SIMPLE case):
validate dax_devlist + famfs_file_bad
walk meta->se[0..fm_nextents-1]:
if local_offset < se[i].ext_len:
dd = devlist[se[i].dev_index]
famfs_dax_err(dd) -> if errored, set meta->error and return
iomap->addr = se[i].ext_offset + local_offset
iomap->offset = file_offset
iomap->length = min(len, ext_len - local_offset)
iomap->dax_dev= dd->devp
iomap->type = IOMAP_MAPPED
return 0
local_offset -= se[i].ext_len
fall-through: zero-length iomap, return -EIO
famfs_fileofs_to_daxofs delegates to famfs_interleave_fileofs_to_daxofs for
INTERLEAVED_EXTENT (see section 6).
7.5 The full iomap call graph
user process fs/fuse/famfs.c fs/dax.c
------------ ---------------- --------
read(2)/write(2)
|
v
fuse_file_read_iter / write_iter (file.c)
|
+--> famfs_fuse_{read,write}_iter
|
+--> famfs_fuse_rw_prep (sanity, truncate to i_size)
|
+--> dax_iomap_rw -----------------> iter loop
|
v
iomap_iter()
|
+--> .iomap_begin <----+
| famfs_fuse_iomap_begin
| \ |
| famfs_fileofs_to_daxofs
| [+ interleave variant]
| \
| fc->dax_devlist[idx]
| dd->devp / ext_offset
| /
+--> dax_iomap_iter()
memcpy via dax_direct_access
on iomap->dax_dev / iomap->addr
page fault on mmap region
|
v
.fault / .huge_fault (famfs_file_vm_ops)
|
+--> __famfs_fuse_filemap_fault
|
+--> dax_iomap_fault(vmf, order, ..., &famfs_iomap_ops)
|
+--> .iomap_begin
| famfs_fuse_iomap_begin
| (resolves dax_dev + offset)
+--> dax_insert_pfn / vmf_insert_pfn_pmd
--------------------------------------------------------------------------------
8. Interleaved (striped) extents
--------------------------------------------------------------------------------
An interleaved extent stripes a contiguous logical region across N strips on
N (typically distinct) dax devices, in fixed-size chunks.
ie_nstrips = N
ie_chunk_size = C (must be PMD-aligned)
ie_nbytes = total logical bytes covered
Logical layout (N=4):
file offset: [0 C ][C 2C][2C 3C][3C 4C][4C ...
| strip 0 | strip 1 | strip 2 | strip 3 | strip 0 ...
| stripe 0 | stripe 0 | stripe 0 | stripe 0 | stripe 1...
Resolution arithmetic in famfs_interleave_fileofs_to_daxofs():
chunk_num = local_offset / chunk_size
chunk_offset = local_offset % chunk_size
chunk_remainder = chunk_size - chunk_offset
stripe_num = chunk_num / nstrips
strip_num = chunk_num % nstrips
strip_offset = chunk_offset + stripe_num * chunk_size
iomap->addr = ie_strips[strip_num].ext_offset + strip_offset
iomap->dax_dev = devlist[ie_strips[strip_num].dev_index].devp
iomap->length = min(len, chunk_remainder)
iomap->type = IOMAP_MAPPED
The length is capped at chunk_remainder so the next iomap iteration steps to
the next chunk (which usually lives on a different device).
--------------------------------------------------------------------------------
9. Memory-error / failure handling
--------------------------------------------------------------------------------
A famfs file becomes unusable when any one of three conditions is true. They
are checked on every read/write/fault by famfs_file_bad() and famfs_dax_err().
Source of error Effect Surface
--------------- ------ -------
fs_dax_get() fails dd->dax_err = 1 famfs_dax_err -> -EIO
notify_failure() upcall dd->error = true famfs_dax_err -> -EHWPOISON
i_size != meta->file_size meta->error = true famfs_file_bad -> -ENXIO
IS_DAX(inode) cleared (size change, etc.) famfs_file_bad -> -ENXIO
notify_failure() flow:
devdax layer detects poison / reconfig
|
v
dax_holder_ops->notify_failure(dax_devp, offset, len, mf_flags)
= famfs_dax_notify_failure
fc = dax_holder(dax_devp)
famfs_set_daxdev_err(fc, dax_devp):
under famfs_devlist_sem (write):
find slot whose dd->devp == dax_devp
dd->error = true
pr_err
On the next iomap_begin, famfs_dax_err sees dd->error and returns -EHWPOISON;
meta->error is also set on that file so subsequent accesses short-circuit
via famfs_file_bad without touching dax.
--------------------------------------------------------------------------------
10. Lifetime / teardown
--------------------------------------------------------------------------------
Per-inode:
fuse_alloc_inode (inode.c)
famfs_meta_set(fi, NULL) (init)
fuse_free_inode (inode.c)
if (S_ISREG && fuse_file_famfs(fi))
famfs_meta_free(fi)
-> __famfs_meta_free: frees se/ie arrays + struct
fuse_evict_inode
if (FUSE_IS_VIRTIO_DAX || fuse_file_famfs)
dax_break_layout_final(inode) (stop ongoing dax mappings)
Per-connection:
fuse_conn_put -> famfs_teardown(fc):
for each valid slot:
if dd->devp:
if (!dd->dax_err) fs_put_dax(dd->devp, fc) /* drop holder */
put_dax(dd->devp)
kfree(dd->name)
kfree(devlist->devlist)
kfree(devlist)
--------------------------------------------------------------------------------
11. Concurrency model
--------------------------------------------------------------------------------
fc->famfs_devlist_sem (rw_semaphore)
readers : iomap_begin paths reading devlist[idx]
famfs_update_daxdev_table while collecting "missing" indices
writers : famfs_fuse_get_daxdev (populating a slot)
famfs_set_daxdev_err (notify_failure)
cmpxchg pairs (NULL -> ptr installation, race-tolerant):
fc->dax_devlist (first-time allocation)
fi->famfs_meta (first GET_FMAP wins, others freed)
wmb() before dd->valid=1 ensures readers that observe `valid` see fully
initialized name/devp/devno fields.
--------------------------------------------------------------------------------
12. End-to-end timeline (read on a freshly opened famfs file)
--------------------------------------------------------------------------------
The "fuse server" column is whatever process is using libfuse (with
op.get_fmap / op.get_daxdev populated as in section 5).
app fuse/famfs (kernel) fuse server
--- ------------------- -----------
open("/mnt/famfs/x") |
----- VFS open -----> fuse_open |
fuse_do_open ----- OPEN -----> handles
<---- ok --------|
fuse_get_fmap |
- GET_FMAP ----->|
<- fmap reply ---|
famfs_fuse_meta_alloc |
famfs_update_daxdev_table |
[new device idx] |
- GET_DAXDEV --->|
<- daxdev reply -|
dax_dev_get + fs_dax_get |
dd->valid = 1 |
famfs_meta_set(fi, meta) |
inode->i_flags |= S_DAX |
i_data.a_ops = famfs_dax_aops |
<----- fd ------------ |
read(fd, buf, len) |
----- VFS read ----> fuse_file_read_iter |
famfs_fuse_read_iter |
famfs_fuse_rw_prep |
dax_iomap_rw |
.iomap_begin -> |
famfs_fuse_iomap_begin |
famfs_fileofs_to_daxofs |
-> iomap{dax_dev, addr} |
memcpy from dax memory |
<---- bytes ---------- |
| <-- no upcall on fast path
munmap / close
----- VFS release -> fuse_release ----- RELEASE ---> |
<---- ok --------|
unmount
----- umount ------> fuse_conn_put |
famfs_teardown |
fs_put_dax / put_dax all dd's |
--------------------------------------------------------------------------------
13. What deliberately is NOT in the kernel
--------------------------------------------------------------------------------
* Allocation and metadata mutation: handled in userspace; the kernel only
consumes fmaps as opaque-but-versioned blobs.
* Page cache and writeback: famfs_dax_aops is exclusively noop_dirty_folio.
* Truncate / append: any size change marks the file errored; recovery is a
userspace responsibility (typically: re-replay the famfs metadata log).
* fallocate / hole handling: files are never sparse and never have holes,
so iomap_begin only ever returns IOMAP_MAPPED (or zero-length on EOF).
* io-modes (FUSE_OPEN_*): bypassed for famfs files in iomode.c since
everything is direct-to-dax.
--------------------------------------------------------------------------------
14. Commit-by-commit map back to this design
--------------------------------------------------------------------------------
14.1 Kernel (linux.git, branch famfs)
ac071fbd94a6 Basic fuse kernel ABI -> Section 4 (negotiation),
CONFIG_FUSE_FAMFS_DAX,
fc->famfs_iomap bit
9a06500c1e0f Plumb GET_FMAP message/response -> Section 6 (fuse_get_fmap,
fuse_open hook)
6f4e03a4e8e9 Create files with famfs fmaps -> Section 3 (famfs_file_meta),
Section 6 (famfs_file_init_dax,
famfs_fuse_meta_alloc)
dfc9e12bcb99 GET_DAXDEV msg + daxdev_table -> Section 3 (famfs_dax_devlist),
Section 6.GET_DAXDEV,
famfs_teardown
d79f803dbfd1 Plumb dax iomap + r/w/mmap -> Section 7 (iomap_ops, fault,
rw paths) and Section 8
(interleave resolver)
8731eb03c762 holder_ops for notify_failure() -> Section 9 (memory errors)
6ea21f89b361 DAX address_space_operations -> Section 1 / famfs_dax_aops
fae4d807da34 fmap metadata documentation -> kernel header comment in
famfs_kfmap.h (Section 8)
da9edf77cbc4 Documentation/filesystems/famfs -> user-facing docs
14.2 libfuse (libfuse.git, branch famfs)
d75ae2ee fuse_kernel.h: bring up to baseline 6.19
Mechanical sync of include/fuse_kernel.h with the kernel uapi
up to 7.45 (everything BEFORE famfs). No new functionality;
this is the baseline the famfs commits build on.
e87be376 fuse_kernel.h: add famfs DAX fmap protocol definitions
Adds protocol 7.46:
* FUSE_DAX_FMAP capability bit
* FUSE_GET_FMAP / FUSE_GET_DAXDEV opcodes
* struct fuse_famfs_fmap_header / simple_ext / iext
* struct fuse_get_daxdev_in / fuse_daxdev_out
* enum fuse_famfs_file_type / famfs_ext_type
Pure header; mirrors include/uapi/linux/fuse.h on the kernel.
-> Section 5.4 (wire formats).
0b16c7d8 fuse: add famfs DAX fmap support
Wires the protocol into the libfuse lowlevel API:
* include/fuse_common.h: FUSE_CAP_DAX_FMAP (1UL << 32)
* include/fuse_lowlevel.h: op.get_fmap, op.get_daxdev
* lib/fuse_lowlevel.c:
- INIT: capable_ext / want_ext <-> FUSE_DAX_FMAP
- dispatch table entries for GET_FMAP / GET_DAXDEV
- do_get_fmap / do_get_daxdev forward to op callbacks
-> Section 4 (capability), Section 5.1-5.3 (libfuse API),
Section 5.5 (server responsibilities).
d1e6135c build(deps): bump github/codeql-action ... (CI; not relevant)
fa03307c doc: replace "futur irrealis"-like tense ... (man pages; not relevant)
9c65d781 Merge branch 'master' into famfs-6.19 (merge commit)
================================================================================
End of document.
================================================================================
^ permalink raw reply
* Re: [PATCH v3 2/2] slab: fix kernel-docs for mm-api
From: Marco Elver @ 2026-05-04 15:00 UTC (permalink / raw)
To: Vlastimil Babka (SUSE)
Cc: Andrew Morton, Jonathan Corbet, Nathan Chancellor, Nicolas Schier,
Dennis Zhou, Tejun Heo, Christoph Lameter, Harry Yoo, Hao Li,
David Rientjes, Roman Gushchin, Kees Cook, Gustavo A. R. Silva,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Alexander Potapenko, Dmitry Vyukov, Nick Desaulniers,
Bill Wendling, Justin Stitt, Miguel Ojeda, linux-kbuild,
linux-kernel, linux-mm, linux-hardening, kasan-dev, llvm,
linux-doc@vger.kernel.org
In-Reply-To: <CANpmjNN_=g31Eoa+w1NrFALfp1dDBi5oHEZdr_bA_48-tS2M=Q@mail.gmail.com>
On Thu, Apr 30, 2026 at 03:59PM +0200, Marco Elver wrote:
> On Thu, 30 Apr 2026 at 15:40, Vlastimil Babka (SUSE) <vbabka@kernel.org> wrote:
> >
> > On 4/24/26 15:24, Marco Elver wrote:
> > > The mm-api kernel-doc comments have been broken for a while, as many
> > > documented symbols shifted from being direct function definitions to
> > > macros wrapping _noprof implementations during the introduction of
> > > allocation tagging (starting with commit 7bd230a26648 "mm/slab: enable
> > > slab allocation tagging for kmalloc and friends").
> > >
> > > When the kernel-doc block remains above the internal implementation
> > > function but uses the public API name, the documentation generator fails
> > > to associate the documented symbol and generates warnings and fails to
> > > emit the documentation.
> > >
> > > Fix this by:
> > >
> > > 1. Moving the kernel-doc comment blocks from slub.c to slab.h, placing
> > > them directly above the user-facing macros.
> > >
> > > 2. Converting the variadic macros for the documented APIs to use
> > > explicit arguments.
> > >
> > > No functional change intended.
> > >
> > > Signed-off-by: Marco Elver <elver@google.com>
> >
> > +Cc Jon
> >
> > I thought it was supposed to work because the kernel-doc scripts were at the
> > time taught by commit 51a7bf0238c2 ("scripts/kernel-doc: drop "_noprof" on
> > function prototypes") to handle _noprof. In the current form git grep finds:
> >
> > tools/lib/python/kdoc/kdoc_parser.py: suffixes = [ '_noprof' ]
> > tools/lib/python/kdoc/xforms_lists.py: (KernRe("_noprof"), ""),
> >
> > Doesn't it work for you then?
>
> Ah, I see. So it doesn't work anymore because we add the '_' prefix, too.
>
> I guess the question is if we want to proliferate more kdoc parser
> special cases, or just move the docs to the macros. The downside of
> macros is that they lose the types in the displayed function
> signature.
>
> Preferences?
How about the below, i.e. adding type decls that only the kernel-doc
parser sees? One complication is also DECL_KMALLOC_PARAMS, and adding
kernel-doc parser hacks for that looks pretty awful, so this is a lot
cleaner.
------ >8 ------
From: Marco Elver <elver@google.com>
Date: Tue, 21 Apr 2026 13:48:21 +0200
Subject: [PATCH] slab: fix kernel-docs for mm-api
The mm-api kernel-docs have been disconnected from their symbols. While
the scripts were previously taught to handle the _noprof suffix added by
allocation tagging (in 51a7bf0238c2 "scripts/kernel-doc: drop "_noprof"
on function prototypes"), this does not handle cases where the internal
implementation function has an additional leading underscore. The added
optional parameters (via DECL_KMALLOC_PARAMS) further complicate parsing
the internal signatures.
When the kernel-doc block remains above the internal implementation
function but uses the public API name, the documentation generator fails
to associate the documented symbol.
Simply moving the docs to the macros in slab.h fixes the association but
causes loss of types in the generated documentation (rendering as e.g.
untyped 'kmalloc(size, flags)' macro).
Fix this by:
1. Moving the kernel-doc comment blocks from slub.c to slab.h, placing
them directly above the user-facing macros.
2. Providing explicit, typed C prototypes for the documented APIs inside
'#if 0 /* kernel-doc */' blocks.
3. Converting the variadic macros for the documented APIs to use
explicit arguments to match the documentation.
No functional change intended.
Signed-off-by: Marco Elver <elver@google.com>
---
v4:
* Provide typed C prototypes in '#if 0' blocks to properly render API.
---
include/linux/slab.h | 216 +++++++++++++++++++++++++++++++++----------
mm/slub.c | 98 --------------------
2 files changed, 168 insertions(+), 146 deletions(-)
diff --git a/include/linux/slab.h b/include/linux/slab.h
index c232f8a10af6..5e1249e36b0d 100644
--- a/include/linux/slab.h
+++ b/include/linux/slab.h
@@ -530,7 +530,49 @@ void * __must_check krealloc_node_align_noprof(const void *objp,
unsigned long align,
gfp_t flags, int nid) __realloc_size(2);
#define krealloc_noprof(_o, _s, _f) krealloc_node_align_noprof(_o, PASS_TOKEN_PARAMS(_s, __kmalloc_token(_s)), 1, _f, NUMA_NO_NODE)
-#define krealloc_node_align(_o, _s, _a, _f, _n) alloc_hooks(krealloc_node_align_noprof(_o, PASS_TOKEN_PARAMS(_s, __kmalloc_token(_s)), _a, _f, _n))
+#if 0 /* kernel-doc */
+/**
+ * krealloc_node_align - reallocate memory. The contents will remain unchanged.
+ * @p: object to reallocate memory for.
+ * @new_size: how many bytes of memory are required.
+ * @align: desired alignment.
+ * @flags: the type of memory to allocate.
+ * @nid: NUMA node or NUMA_NO_NODE
+ *
+ * If @p is %NULL, krealloc() behaves exactly like kmalloc(). If @new_size
+ * is 0 and @p is not a %NULL pointer, the object pointed to is freed.
+ *
+ * Only alignments up to those guaranteed by kmalloc() will be honored. Please see
+ * Documentation/core-api/memory-allocation.rst for more details.
+ *
+ * If __GFP_ZERO logic is requested, callers must ensure that, starting with the
+ * initial memory allocation, every subsequent call to this API for the same
+ * memory allocation is flagged with __GFP_ZERO. Otherwise, it is possible that
+ * __GFP_ZERO is not fully honored by this API.
+ *
+ * When slub_debug_orig_size() is off, krealloc() only knows about the bucket
+ * size of an allocation (but not the exact size it was allocated with) and
+ * hence implements the following semantics for shrinking and growing buffers
+ * with __GFP_ZERO::
+ *
+ * new bucket
+ * 0 size size
+ * |--------|----------------|
+ * | keep | zero |
+ *
+ * Otherwise, the original allocation size 'orig_size' could be used to
+ * precisely clear the requested size, and the new size will also be stored
+ * as the new 'orig_size'.
+ *
+ * In any case, the contents of the object pointed to are preserved up to the
+ * lesser of the new and old sizes.
+ *
+ * Return: pointer to the allocated memory or %NULL in case of error
+ */
+void *krealloc_node_align(const void *p, size_t new_size, unsigned long align, gfp_t flags, int nid);
+#endif
+#define krealloc_node_align(p, new_size, align, flags, nid) \
+ alloc_hooks(krealloc_node_align_noprof(p, PASS_TOKEN_PARAMS(new_size, __kmalloc_token(new_size)), align, flags, nid))
#define krealloc_node(_o, _s, _f, _n) krealloc_node_align(_o, _s, 1, _f, _n)
#define krealloc(...) krealloc_node(__VA_ARGS__, NUMA_NO_NODE)
@@ -913,6 +955,23 @@ void *__kmalloc_large_noprof(size_t size, gfp_t flags)
void *__kmalloc_large_node_noprof(size_t size, gfp_t flags, int node)
__assume_page_alignment __alloc_size(1);
+static __always_inline __alloc_size(1) void *_kmalloc_noprof(size_t size, gfp_t flags, kmalloc_token_t token)
+{
+ if (__builtin_constant_p(size) && size) {
+ unsigned int index;
+
+ if (size > KMALLOC_MAX_CACHE_SIZE)
+ return __kmalloc_large_noprof(size, flags);
+
+ index = kmalloc_index(size);
+ return __kmalloc_cache_noprof(
+ kmalloc_caches[kmalloc_type(flags, token)][index],
+ flags, size);
+ }
+ return __kmalloc_noprof(PASS_KMALLOC_PARAMS(size, NULL, token), flags);
+}
+#define kmalloc_noprof(...) _kmalloc_noprof(__VA_ARGS__, __kmalloc_token(__VA_ARGS__))
+#if 0 /* kernel-doc */
/**
* kmalloc - allocate kernel memory
* @size: how many bytes of memory are required.
@@ -968,27 +1027,27 @@ void *__kmalloc_large_node_noprof(size_t size, gfp_t flags, int node)
* Try really hard to succeed the allocation but fail
* eventually.
*/
-static __always_inline __alloc_size(1) void *_kmalloc_noprof(size_t size, gfp_t flags, kmalloc_token_t token)
-{
- if (__builtin_constant_p(size) && size) {
- unsigned int index;
-
- if (size > KMALLOC_MAX_CACHE_SIZE)
- return __kmalloc_large_noprof(size, flags);
-
- index = kmalloc_index(size);
- return __kmalloc_cache_noprof(
- kmalloc_caches[kmalloc_type(flags, token)][index],
- flags, size);
- }
- return __kmalloc_noprof(PASS_KMALLOC_PARAMS(size, NULL, token), flags);
-}
-#define kmalloc_noprof(...) _kmalloc_noprof(__VA_ARGS__, __kmalloc_token(__VA_ARGS__))
-#define kmalloc(...) alloc_hooks(kmalloc_noprof(__VA_ARGS__))
+void *kmalloc(size_t size, gfp_t flags);
+#endif
+#define kmalloc(size, flags) alloc_hooks(kmalloc_noprof(size, flags))
void *_kmalloc_nolock_noprof(DECL_TOKEN_PARAMS(size, token), gfp_t gfp_flags, int node);
#define kmalloc_nolock_noprof(_s, _f, _n) _kmalloc_nolock_noprof(PASS_TOKEN_PARAMS(_s, __kmalloc_token(_s)), _f, _n)
-#define kmalloc_nolock(...) alloc_hooks(kmalloc_nolock_noprof(__VA_ARGS__))
+#if 0 /* kernel-doc */
+/**
+ * kmalloc_nolock - Allocate an object of given size from any context.
+ * @size: size to allocate
+ * @gfp_flags: GFP flags. Only __GFP_ACCOUNT, __GFP_ZERO, __GFP_NO_OBJ_EXT
+ * allowed.
+ * @node: node number of the target node.
+ *
+ * Return: pointer to the new object or NULL in case of error.
+ * NULL does not mean EBUSY or EAGAIN. It means ENOMEM.
+ * There is no reason to call it again and expect !NULL.
+ */
+void *kmalloc_nolock(size_t size, gfp_t gfp_flags, int node);
+#endif
+#define kmalloc_nolock(size, gfp_flags, node) alloc_hooks(kmalloc_nolock_noprof(size, gfp_flags, node))
/**
* __alloc_objs - Allocate objects of a given type using
@@ -1115,23 +1174,40 @@ static __always_inline __alloc_size(1) void *_kmalloc_node_noprof(size_t size, g
#define kmalloc_node_noprof(...) _kmalloc_node_noprof(__VA_ARGS__, __kmalloc_token(__VA_ARGS__))
#define kmalloc_node(...) alloc_hooks(kmalloc_node_noprof(__VA_ARGS__))
+static inline __alloc_size(1, 2) void *_kmalloc_array_noprof(size_t n, size_t size, gfp_t flags, kmalloc_token_t token)
+{
+ size_t bytes;
+
+ if (unlikely(check_mul_overflow(n, size, &bytes)))
+ return NULL;
+ return _kmalloc_noprof(bytes, flags, token);
+}
+#define kmalloc_array_noprof(...) _kmalloc_array_noprof(__VA_ARGS__, __kmalloc_token(__VA_ARGS__))
+#if 0 /* kernel-doc */
/**
* kmalloc_array - allocate memory for an array.
* @n: number of elements.
* @size: element size.
* @flags: the type of memory to allocate (see kmalloc).
*/
-static inline __alloc_size(1, 2) void *_kmalloc_array_noprof(size_t n, size_t size, gfp_t flags, kmalloc_token_t token)
+void *kmalloc_array(size_t n, size_t size, gfp_t flags);
+#endif
+#define kmalloc_array(n, size, flags) alloc_hooks(kmalloc_array_noprof(n, size, flags))
+
+static inline __realloc_size(2, 3) void * __must_check _krealloc_array_noprof(void *p,
+ size_t new_n,
+ size_t new_size,
+ gfp_t flags, kmalloc_token_t token)
{
size_t bytes;
- if (unlikely(check_mul_overflow(n, size, &bytes)))
+ if (unlikely(check_mul_overflow(new_n, new_size, &bytes)))
return NULL;
- return _kmalloc_noprof(bytes, flags, token);
-}
-#define kmalloc_array_noprof(...) _kmalloc_array_noprof(__VA_ARGS__, __kmalloc_token(__VA_ARGS__))
-#define kmalloc_array(...) alloc_hooks(kmalloc_array_noprof(__VA_ARGS__))
+ return krealloc_node_align_noprof(p, PASS_TOKEN_PARAMS(bytes, token), 1, flags, NUMA_NO_NODE);
+}
+#define krealloc_array_noprof(...) _krealloc_array_noprof(__VA_ARGS__, __kmalloc_token(__VA_ARGS__))
+#if 0 /* kernel-doc */
/**
* krealloc_array - reallocate memory for an array.
* @p: pointer to the memory chunk to reallocate
@@ -1149,20 +1225,9 @@ static inline __alloc_size(1, 2) void *_kmalloc_array_noprof(size_t n, size_t si
* In any case, the contents of the object pointed to are preserved up to the
* lesser of the new and old sizes.
*/
-static inline __realloc_size(2, 3) void * __must_check _krealloc_array_noprof(void *p,
- size_t new_n,
- size_t new_size,
- gfp_t flags, kmalloc_token_t token)
-{
- size_t bytes;
-
- if (unlikely(check_mul_overflow(new_n, new_size, &bytes)))
- return NULL;
-
- return krealloc_node_align_noprof(p, PASS_TOKEN_PARAMS(bytes, token), 1, flags, NUMA_NO_NODE);
-}
-#define krealloc_array_noprof(...) _krealloc_array_noprof(__VA_ARGS__, __kmalloc_token(__VA_ARGS__))
-#define krealloc_array(...) alloc_hooks(krealloc_array_noprof(__VA_ARGS__))
+void *krealloc_array(void *p, size_t new_n, size_t new_size, gfp_t flags);
+#endif
+#define krealloc_array(p, new_n, new_size, flags) alloc_hooks(krealloc_array_noprof(p, new_n, new_size, flags))
/**
* kcalloc - allocate memory for an array. The memory is set to zero.
@@ -1214,17 +1279,20 @@ static inline __alloc_size(1, 2) void *_kmalloc_array_node_noprof(size_t n, size
*/
#define kmem_cache_zalloc(_k, _flags) kmem_cache_alloc(_k, (_flags)|__GFP_ZERO)
-/**
- * kzalloc - allocate memory. The memory is set to zero.
- * @size: how many bytes of memory are required.
- * @flags: the type of memory to allocate (see kmalloc).
- */
static inline __alloc_size(1) void *_kzalloc_noprof(size_t size, gfp_t flags, kmalloc_token_t token)
{
return _kmalloc_noprof(size, flags | __GFP_ZERO, token);
}
#define kzalloc_noprof(...) _kzalloc_noprof(__VA_ARGS__, __kmalloc_token(__VA_ARGS__))
-#define kzalloc(...) alloc_hooks(kzalloc_noprof(__VA_ARGS__))
+#if 0 /* kernel-doc */
+/**
+ * kzalloc - allocate memory. The memory is set to zero.
+ * @size: how many bytes of memory are required.
+ * @flags: the type of memory to allocate (see kmalloc).
+ */
+void *kzalloc(size_t size, gfp_t flags);
+#endif
+#define kzalloc(size, flags) alloc_hooks(kzalloc_noprof(size, flags))
#define kzalloc_node(_size, _flags, _node) kmalloc_node(_size, (_flags)|__GFP_ZERO, _node)
void *__kvmalloc_node_noprof(DECL_KMALLOC_PARAMS(size, b, token), unsigned long align,
@@ -1233,7 +1301,29 @@ void *__kvmalloc_node_noprof(DECL_KMALLOC_PARAMS(size, b, token), unsigned long
__kvmalloc_node_noprof(PASS_KMALLOC_PARAMS(_size, NULL, __kmalloc_token(_size)), _align, _flags, _node)
#define kvmalloc_node_align(...) \
alloc_hooks(kvmalloc_node_align_noprof(__VA_ARGS__))
-#define kvmalloc_node(_s, _f, _n) kvmalloc_node_align(_s, 1, _f, _n)
+#if 0 /* kernel-doc */
+/**
+ * kvmalloc_node - attempt to allocate physically contiguous memory, but upon
+ * failure, fall back to non-contiguous (vmalloc) allocation.
+ * @size: size of the request.
+ * @flags: gfp mask for the allocation - must be compatible (superset) with GFP_KERNEL.
+ * @node: numa node to allocate from
+ *
+ * Only alignments up to those guaranteed by kmalloc() will be honored. Please see
+ * Documentation/core-api/memory-allocation.rst for more details.
+ *
+ * Uses kmalloc to get the memory but if the allocation fails then falls back
+ * to the vmalloc allocator. Use kvfree for freeing the memory.
+ *
+ * GFP_NOWAIT and GFP_ATOMIC are supported, the __GFP_NORETRY modifier is not.
+ * __GFP_RETRY_MAYFAIL is supported, and it should be used only if kmalloc is
+ * preferable to the vmalloc fallback, due to visible performance drawbacks.
+ *
+ * Return: pointer to the allocated memory of %NULL in case of failure
+ */
+void *kvmalloc_node(size_t size, gfp_t flags, int node);
+#endif
+#define kvmalloc_node(size, flags, node) kvmalloc_node_align(size, 1, flags, node)
#define kvmalloc_node_noprof(size, flags, node) \
kvmalloc_node_align_noprof(size, 1, flags, node)
#define kvmalloc(...) kvmalloc_node(__VA_ARGS__, NUMA_NO_NODE)
@@ -1266,8 +1356,38 @@ _kvmalloc_array_node_noprof(size_t n, size_t size, gfp_t flags, int node, kmallo
void *kvrealloc_node_align_noprof(const void *p, DECL_TOKEN_PARAMS(size, token), unsigned long align,
gfp_t flags, int nid) __realloc_size(2);
-#define kvrealloc_node_align(_p, _s, _a, _f, _n) \
- alloc_hooks(kvrealloc_node_align_noprof(_p, PASS_TOKEN_PARAMS(_s, __kmalloc_token(_s)), _a, _f, _n))
+#if 0 /* kernel-doc */
+/**
+ * kvrealloc_node_align - reallocate memory; contents remain unchanged
+ * @p: object to reallocate memory for
+ * @size: the size to reallocate
+ * @align: desired alignment
+ * @flags: the flags for the page level allocator
+ * @nid: NUMA node id
+ *
+ * If @p is %NULL, kvrealloc() behaves exactly like kvmalloc(). If @size is 0
+ * and @p is not a %NULL pointer, the object pointed to is freed.
+ *
+ * Only alignments up to those guaranteed by kmalloc() will be honored. Please see
+ * Documentation/core-api/memory-allocation.rst for more details.
+ *
+ * If __GFP_ZERO logic is requested, callers must ensure that, starting with the
+ * initial memory allocation, every subsequent call to this API for the same
+ * memory allocation is flagged with __GFP_ZERO. Otherwise, it is possible that
+ * __GFP_ZERO is not fully honored by this API.
+ *
+ * In any case, the contents of the object pointed to are preserved up to the
+ * lesser of the new and old sizes.
+ *
+ * This function must not be called concurrently with itself or kvfree() for the
+ * same memory allocation.
+ *
+ * Return: pointer to the allocated memory or %NULL in case of error
+ */
+void *kvrealloc_node_align(const void *p, size_t size, unsigned long align, gfp_t flags, int nid);
+#endif
+#define kvrealloc_node_align(p, size, align, flags, nid) \
+ alloc_hooks(kvrealloc_node_align_noprof(p, PASS_TOKEN_PARAMS(size, __kmalloc_token(size)), align, flags, nid))
#define kvrealloc_node(_p, _s, _f, _n) kvrealloc_node_align(_p, _s, 1, _f, _n)
#define kvrealloc(...) kvrealloc_node(__VA_ARGS__, NUMA_NO_NODE)
diff --git a/mm/slub.c b/mm/slub.c
index ccb208cfbecd..dbc3c947e5be 100644
--- a/mm/slub.c
+++ b/mm/slub.c
@@ -5310,17 +5310,6 @@ void *__kmalloc_noprof(DECL_KMALLOC_PARAMS(size, b, token), gfp_t flags)
}
EXPORT_SYMBOL(__kmalloc_noprof);
-/**
- * kmalloc_nolock - Allocate an object of given size from any context.
- * @size: size to allocate
- * @gfp_flags: GFP flags. Only __GFP_ACCOUNT, __GFP_ZERO, __GFP_NO_OBJ_EXT
- * allowed.
- * @node: node number of the target node.
- *
- * Return: pointer to the new object or NULL in case of error.
- * NULL does not mean EBUSY or EAGAIN. It means ENOMEM.
- * There is no reason to call it again and expect !NULL.
- */
void *_kmalloc_nolock_noprof(DECL_TOKEN_PARAMS(size, token), gfp_t gfp_flags, int node)
{
gfp_t alloc_gfp = __GFP_NOWARN | __GFP_NOMEMALLOC | gfp_flags;
@@ -6717,44 +6706,6 @@ __do_krealloc(const void *p, size_t new_size, unsigned long align, gfp_t flags,
return ret;
}
-/**
- * krealloc_node_align - reallocate memory. The contents will remain unchanged.
- * @p: object to reallocate memory for.
- * @new_size: how many bytes of memory are required.
- * @align: desired alignment.
- * @flags: the type of memory to allocate.
- * @nid: NUMA node or NUMA_NO_NODE
- *
- * If @p is %NULL, krealloc() behaves exactly like kmalloc(). If @new_size
- * is 0 and @p is not a %NULL pointer, the object pointed to is freed.
- *
- * Only alignments up to those guaranteed by kmalloc() will be honored. Please see
- * Documentation/core-api/memory-allocation.rst for more details.
- *
- * If __GFP_ZERO logic is requested, callers must ensure that, starting with the
- * initial memory allocation, every subsequent call to this API for the same
- * memory allocation is flagged with __GFP_ZERO. Otherwise, it is possible that
- * __GFP_ZERO is not fully honored by this API.
- *
- * When slub_debug_orig_size() is off, krealloc() only knows about the bucket
- * size of an allocation (but not the exact size it was allocated with) and
- * hence implements the following semantics for shrinking and growing buffers
- * with __GFP_ZERO::
- *
- * new bucket
- * 0 size size
- * |--------|----------------|
- * | keep | zero |
- *
- * Otherwise, the original allocation size 'orig_size' could be used to
- * precisely clear the requested size, and the new size will also be stored
- * as the new 'orig_size'.
- *
- * In any case, the contents of the object pointed to are preserved up to the
- * lesser of the new and old sizes.
- *
- * Return: pointer to the allocated memory or %NULL in case of error
- */
void *krealloc_node_align_noprof(const void *p, DECL_TOKEN_PARAMS(new_size, token), unsigned long align,
gfp_t flags, int nid)
{
@@ -6797,28 +6748,6 @@ static gfp_t kmalloc_gfp_adjust(gfp_t flags, size_t size)
return flags;
}
-/**
- * __kvmalloc_node - attempt to allocate physically contiguous memory, but upon
- * failure, fall back to non-contiguous (vmalloc) allocation.
- * @size: size of the request.
- * @b: which set of kmalloc buckets to allocate from.
- * @token: allocation token.
- * @align: desired alignment.
- * @flags: gfp mask for the allocation - must be compatible (superset) with GFP_KERNEL.
- * @node: numa node to allocate from
- *
- * Only alignments up to those guaranteed by kmalloc() will be honored. Please see
- * Documentation/core-api/memory-allocation.rst for more details.
- *
- * Uses kmalloc to get the memory but if the allocation fails then falls back
- * to the vmalloc allocator. Use kvfree for freeing the memory.
- *
- * GFP_NOWAIT and GFP_ATOMIC are supported, the __GFP_NORETRY modifier is not.
- * __GFP_RETRY_MAYFAIL is supported, and it should be used only if kmalloc is
- * preferable to the vmalloc fallback, due to visible performance drawbacks.
- *
- * Return: pointer to the allocated memory of %NULL in case of failure
- */
void *__kvmalloc_node_noprof(DECL_KMALLOC_PARAMS(size, b, token), unsigned long align,
gfp_t flags, int node)
{
@@ -6900,33 +6829,6 @@ void kvfree_sensitive(const void *addr, size_t len)
}
EXPORT_SYMBOL(kvfree_sensitive);
-/**
- * kvrealloc_node_align - reallocate memory; contents remain unchanged
- * @p: object to reallocate memory for
- * @size: the size to reallocate
- * @align: desired alignment
- * @flags: the flags for the page level allocator
- * @nid: NUMA node id
- *
- * If @p is %NULL, kvrealloc() behaves exactly like kvmalloc(). If @size is 0
- * and @p is not a %NULL pointer, the object pointed to is freed.
- *
- * Only alignments up to those guaranteed by kmalloc() will be honored. Please see
- * Documentation/core-api/memory-allocation.rst for more details.
- *
- * If __GFP_ZERO logic is requested, callers must ensure that, starting with the
- * initial memory allocation, every subsequent call to this API for the same
- * memory allocation is flagged with __GFP_ZERO. Otherwise, it is possible that
- * __GFP_ZERO is not fully honored by this API.
- *
- * In any case, the contents of the object pointed to are preserved up to the
- * lesser of the new and old sizes.
- *
- * This function must not be called concurrently with itself or kvfree() for the
- * same memory allocation.
- *
- * Return: pointer to the allocated memory or %NULL in case of error
- */
void *kvrealloc_node_align_noprof(const void *p, DECL_TOKEN_PARAMS(size, token), unsigned long align,
gfp_t flags, int nid)
{
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related
* Re: [PATCH] crypto: af_alg - Document the deprecation of AF_ALG
From: Jon Kohler @ 2026-05-04 14:39 UTC (permalink / raw)
To: Eric Biggers
Cc: linux-crypto@vger.kernel.org, Herbert Xu,
linux-doc@vger.kernel.org, linux-api@vger.kernel.org,
linux-kernel@vger.kernel.org, netdev@vger.kernel.org,
Linus Torvalds
In-Reply-To: <20260430011544.31823-1-ebiggers@kernel.org>
> On Apr 29, 2026, at 9:15 PM, Eric Biggers <ebiggers@kernel.org> wrote:
>
> AF_ALG is almost completely unnecessary, and it exposes a massive attack
> surface that hasn't been standing up to modern vulnerability discovery
> tools. The latest one even has its own website, providing a small
> Python script that reliably roots most Linux distros: https://copy.fail/
>
> This isn't sustainable, especially as LLMs have accelerated the rate the
> vulnerabilities are coming in. The effort that is being put into this
> thing is vastly disproportional to the few programs that actually use
> it, and those programs would be better served by userspace code anyway.
>
> These issues have been noted in many mailing list discussions already.
> But until now they haven't been reflected in the documentation or
> kconfig menu itself, and the vulnerabilities are still coming in.
>
> Let's go ahead and document the deprecation.
>
> This isn't intended to change anything overnight. After all, most Linux
> distros won't be able to disable the kconfig options quite yet, mainly
> because of iwd. But this should create a bit more impetus for these
> userspace programs to be fixed, and the documentation update should also
> help prevent more users from appearing.
>
> Signed-off-by: Eric Biggers <ebiggers@kernel.org>
> ---
Quick passing observation
I noticed that when attempting to completely disable these Crypto APIs,
I was experiencing boot failures with fips=1 enabled systems.
Using 6.18-based kernel with an el9-based user space, I see the
following hang in the early boot console from dracut-pre-pivot:
Check integrity of kernel
libkcapi - Error: AF_ALG: socket syscall failed (errno: -97)
Allocation of hmac(sha512) cipher failed (-97)
I haven't looked at every elX version, but at least in el9 and el10,
they use libkcapi-hmaccalc to provide sha512hmac, which dracut [1]
uses to calculate the HMAC value in do_fips().
Digging further, I was only able to disable RNG and AEAD APIs, but
not HASH and SKCIPHER APIs when FIPS was in the picture with el9++.
I’m not sure how other distros do the same, but this could be problematic
elsehwere if other distros went down the libkcapi route.
[1] https://github.com/dracutdevs/dracut/blob/059/modules.d/01fips/fips.sh#L167
^ permalink raw reply
* [PATCH v2] Documentation: leds: leds-class: Document keyboard backlight LED class naming
From: Hans de Goede @ 2026-05-04 14:54 UTC (permalink / raw)
To: Lee Jones, Pavel Machek, Jonathan Corbet, Shuah Khan
Cc: Hans de Goede, Rishit Bansal, Carlos Ferreira, Edip Hazuri,
Mustafa Ekşi, Xavier Bestel, linux-doc, linux-leds,
Kate Hsuan
From: Carlos Ferreira <carlosmiguelferreira.2003@gmail.com>
Document the existing practice of always using 'kbd_backlight' for
the function part of LED class device names for LED class devices which
control single-zone keyboard backlights.
Also extend this existing practice with a new naming scheme for keyboards
with zoned backlight control. There are several drivers in the works (see
the Link:tags below) which offer backlight control for keyboards where
the keyboard backlight is divided in a limited number of zones, e.g.
"main", "cursor" and "numpad" zones.
It is important to agree on a consistent naming scheme for these now,
so that userspace can support multiple different models / vendors through
a single unified naming scheme.
Link: https://lore.kernel.org/platform-driver-x86/20230131235027.36304-1-rishitbansal0@gmail.com/
Link: https://lore.kernel.org/platform-driver-x86/20240719100011.16656-1-carlosmiguelferreira.2003@gmail.com/
Link: https://lore.kernel.org/platform-driver-x86/20260304105831.119349-3-edip@medip.dev/
Link: https://lore.kernel.org/platform-driver-x86/20240806205001.191551-2-mustafa.eskieksi@gmail.com/
Link: https://lore.kernel.org/linux-input/20260402075239.3829699-1-xav@bes.tel/
Acked-by: Kate Hsuan <hpa@redhat.com>
Signed-off-by: Carlos Ferreira <carlosmiguelferreira.2003@gmail.com>
Co-developed-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
Signed-off-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
---
Changes in v2:
- s/compatiblity/compatibility/
---
Documentation/leds/leds-class.rst | 63 +++++++++++++++++++++++++++++++
1 file changed, 63 insertions(+)
diff --git a/Documentation/leds/leds-class.rst b/Documentation/leds/leds-class.rst
index 5db620ed27aa..3913966cfdac 100644
--- a/Documentation/leds/leds-class.rst
+++ b/Documentation/leds/leds-class.rst
@@ -116,6 +116,69 @@ above leaves scope for further attributes should they be needed. If sections
of the name don't apply, just leave that section blank.
+Keyboard backlight control LED Device Naming
+============================================
+
+For backlit keyboards with a single brightness / color settings a single
+(multicolor) LED class device should be used to allow userspace to change
+the backlight brightness (and if possible the color). This LED class device
+must use "kbd_backlight" for the function part of the LED class device name.
+IOW the name must end with ":kbd_backlight".
+
+For backlit keyboards with multiple control zones, one (multicolor) LED class
+device should be used per zone. These LED class devices' name must follow:
+
+ "<devicename>:<color>:kbd_zoned_backlight-<zone_name>"
+
+and <devicename> must be the same for all zones of the same keyboard.
+
+<zone_name> should be descriptive of which part of the keyboard backlight
+the zone covers and should be suitable for userspace to show to an end user
+in an UI for controlling the zones.
+
+Where possible <zone_name> should be a value already used by other
+zoned keyboards with a similar or identical zone layout, e.g.:
+
+<devicename>:<color>:kbd_zoned_backlight-right
+<devicename>:<color>:kbd_zoned_backlight-middle
+<devicename>:<color>:kbd_zoned_backlight-left
+<devicename>:<color>:kbd_zoned_backlight-corners
+<devicename>:<color>:kbd_zoned_backlight-wasd
+
+or:
+
+<devicename>:<color>:kbd_zoned_backlight-main
+<devicename>:<color>:kbd_zoned_backlight-cursor
+<devicename>:<color>:kbd_zoned_backlight-numpad
+<devicename>:<color>:kbd_zoned_backlight-corners
+<devicename>:<color>:kbd_zoned_backlight-wasd
+
+Note that this is intended for keyboards with a limited number of zones,
+keyboards with per key addressable backlighting must not use LED class devices
+since the sysfs API is not suitable for rapidly change multiple LEDs in one
+"commit" as is necessary to do animations / special effects on such keyboards.
+
+An exception to the rule that all zones must follow:
+
+ "<devicename>:<color>:kbd_zoned_backlight-<zone_name>"
+
+is made for the special case where there is a single big zone which controls
+the backlighting of almost all of the keyboard and there are some small areas
+with separate control, like just the 4 cursor keys, or the WASD keys. In this
+case the main zone should use 'kbd_backlight' for the function part of the name
+for compatibility with (older) userspace code which is not aware of
+the "kbd_zoned_backlight-<zone_name>" function naming scheme.
+
+While the smaller zones should use the new zoned naming scheme. Such a setup
+would result in e.g.:
+
+<devicename>:<color>:kbd_backlight
+<devicename>:<color>:kbd_zoned_backlight-wasd
+
+"kbd_zoned_backlight-<zone_name>" aware userspace should be aware of this
+exception and check for a main zone with a "kbd_backlight" function-name.
+
+
Brightness setting API
======================
--
2.54.0
^ permalink raw reply related
* Re: [PATCH 1/1] Documentation: leds: leds-class: Document keyboard backlight LED class naming
From: Hans de Goede @ 2026-05-04 14:54 UTC (permalink / raw)
To: Lee Jones
Cc: Pavel Machek, Jonathan Corbet, Shuah Khan, Rishit Bansal,
Carlos Ferreira, Edip Hazuri, Mustafa Ekşi, Xavier Bestel,
linux-leds, linux-doc
In-Reply-To: <20260430092508.GF1806155@google.com>
Hi Lee,
On 30-Apr-26 11:25 AM, Lee Jones wrote:
> On Mon, 06 Apr 2026, Hans de Goede wrote:
>
>> From: Carlos Ferreira <carlosmiguelferreira.2003@gmail.com>
>>
>> Document the existing practice of always using 'kbd_backlight' for
>> the function part of LED class device names for LED class devices which
>> control single-zone keyboard backlights.
>>
>> Also extend this existing practice with a new naming scheme for keyboards
>> with zoned backlight control. There are several drivers in the works (see
>> the Link:tags below) which offer backlight control for keyboards where
>> the keyboard backlight is divided in a limited number of zones, e.g.
>> "main", "cursor" and "numpad" zones.
>>
>> It is important to agree on a consistent naming scheme for these now,
>> so that userspace can support multiple different models / vendors through
>> a single unified naming scheme.
>>
>> Link: https://lore.kernel.org/platform-driver-x86/20230131235027.36304-1-rishitbansal0@gmail.com/
>> Link: https://lore.kernel.org/platform-driver-x86/20240719100011.16656-1-carlosmiguelferreira.2003@gmail.com/
>> Link: https://lore.kernel.org/platform-driver-x86/20260304105831.119349-3-edip@medip.dev/
>> Link: https://lore.kernel.org/platform-driver-x86/20240806205001.191551-2-mustafa.eskieksi@gmail.com/
>> Link: https://lore.kernel.org/linux-input/20260402075239.3829699-1-xav@bes.tel/
>> Signed-off-by: Carlos Ferreira <carlosmiguelferreira.2003@gmail.com>
>> Co-authored-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
>> Signed-off-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
>
> The premise is fine I think.
Great, thank you for taking a look.
>> ---
>> Documentation/leds/leds-class.rst | 63 +++++++++++++++++++++++++++++++
>> 1 file changed, 63 insertions(+)
>>
>> diff --git a/Documentation/leds/leds-class.rst b/Documentation/leds/leds-class.rst
>> index 5db620ed27aa..d2b042519a66 100644
>> --- a/Documentation/leds/leds-class.rst
>> +++ b/Documentation/leds/leds-class.rst
>> @@ -116,6 +116,69 @@ above leaves scope for further attributes should they be needed. If sections
>> of the name don't apply, just leave that section blank.
>>
>>
>> +Keyboard backlight control LED Device Naming
>> +============================================
>> +
>> +For backlit keyboards with a single brightness / color settings a single
>> +(multicolor) LED class device should be used to allow userspace to change
>> +the backlight brightness (and if possible the color). This LED class device
>> +must use "kbd_backlight" for the function part of the LED class device name.
>> +IOW the name must end with ":kbd_backlight".
>> +
>> +For backlit keyboards with multiple control zones, one (multicolor) LED class
>> +device should be used per zone. These LED class devices' name must follow:
>> +
>> + "<devicename>:<color>:kbd_zoned_backlight-<zone_name>"
>> +
>> +and <devicename> must be the same for all zones of the same keyboard.
>> +
>> +<zone_name> should be descriptive of which part of the keyboard backlight
>> +the zone covers and should be suitable for userspace to show to an end user
>> +in an UI for controlling the zones.
>> +
>> +Where possible <zone_name> should be a value already used by other
>> +zoned keyboards with a similar or identical zone layout, e.g.:
>> +
>> +<devicename>:<color>:kbd_zoned_backlight-right
>> +<devicename>:<color>:kbd_zoned_backlight-middle
>> +<devicename>:<color>:kbd_zoned_backlight-left
>> +<devicename>:<color>:kbd_zoned_backlight-corners
>> +<devicename>:<color>:kbd_zoned_backlight-wasd
>> +
>> +or:
>> +
>> +<devicename>:<color>:kbd_zoned_backlight-main
>> +<devicename>:<color>:kbd_zoned_backlight-cursor
>> +<devicename>:<color>:kbd_zoned_backlight-numpad
>> +<devicename>:<color>:kbd_zoned_backlight-corners
>> +<devicename>:<color>:kbd_zoned_backlight-wasd
>> +
>> +Note that this is intended for keyboards with a limited number of zones,
>> +keyboards with per key addressable backlighting must not use LED class devices
>> +since the sysfs API is not suitable for rapidly change multiple LEDs in one
>> +"commit" as is necessary to do animations / special effects on such keyboards.
>> +
>> +An exception to the rule that all zones must follow:
>> +
>> + "<devicename>:<color>:kbd_zoned_backlight-<zone_name>"
>> +
>> +is made for the special case where there is a single big zone which controls
>> +the backlighting of almost all of the keyboard and there are some small areas
>> +with separate control, like just the 4 cursor keys, or the WASD keys. In this
>> +case the main zone should use 'kbd_backlight' for the function part of the name
>> +for compatiblity with (older) userspace code which is not aware of
>
> Nit: compatibility
>
> There may be others. Please run it through a spell checker.
Ack, I'll send a v2 fixing this. I've run the patch through hunspell and this
was the only spelling issue it found.
Regards,
Hans
>
>> +the "kbd_zoned_backlight-<zone_name>" function naming scheme.
>> +
>> +While the smaller zones should use the new zoned naming scheme. Such a setup
>> +would result in e.g.:
>> +
>> +<devicename>:<color>:kbd_backlight
>> +<devicename>:<color>:kbd_zoned_backlight-wasd
>> +
>> +"kbd_zoned_backlight-<zone_name>" aware userspace should be aware of this
>> +exception and check for a main zone with a "kbd_backlight" function-name.
>> +
>> +
>> Brightness setting API
>> ======================
>>
>> --
>> 2.53.0
>>
>
^ permalink raw reply
* Re: [PATCH v4 00/10] Auto-generate maintainer profile entries
From: Mauro Carvalho Chehab @ 2026-05-04 14:39 UTC (permalink / raw)
To: Jonathan Corbet
Cc: Albert Ou, Mauro Carvalho Chehab, Palmer Dabbelt, Paul Walmsley,
linux-doc, linux-kernel, linux-riscv, workflows, Alexandre Ghiti,
Shuah Khan, Randy Dunlap, Dan Williams
In-Reply-To: <20260504090041.243520a8@foz.lan>
On Mon, 4 May 2026 09:00:41 +0200
Mauro Carvalho Chehab <mchehab+huawei@kernel.org> wrote:
> On Sun, 03 May 2026 09:49:41 -0600
> Jonathan Corbet <corbet@lwn.net> wrote:
>
> > Mauro Carvalho Chehab <mchehab+huawei@kernel.org> writes:
> >
> > > Hi Jon,
> > >
> > > This is basically the same patch series I sent during the merge
> > > window, rebased on the top of post 7.1-rc1 docs-next branch.
> > > It is tested both with and without O=DOCS.
> > >
> > > It contains just one extra trivial patch adding a missing SPDX
> > > header, and, on v4, I dropped two patches touching MAINTAINERS,
> > > as those aren't needed anymore.
> > >
> > > This patch series change the way maintainer entry profile links
> > > are added to the documentation. Instead of having an entry for
> > > each of them at an ReST file, get them from MAINTAINERS content.
> > >
> > > That should likely make easier to maintain, as there will be a single
> > > point to place all such profiles.
> > >
> > > The output is a per-subsystem sorted (*) series of links shown as a
> > > list like this:
> > >
> > > - Arm And Arm64 Soc Sub-Architectures (Common Parts)
> > > - Arm/Samsung S3C, S5P And Exynos Arm Architectures
> > > - Arm/Tesla Fsd Soc Support
> > > ...
> > > - Xfs Filesystem
> > >
> > > Please notice that the series is doing one logical change per patch.
> > > I could have merged some changes altogether, but I opted doing it
> > > in small steps to help reviews. If you prefer, feel free to merge
> > > maintainers_include changes on merge.
> > >
> > > There is one interesting side effect of this series: there is no
> > > need to add rst files containing profiles inside a TOC tree: Just
> > > creating the file anywhere inside Documentation and adding a P entry
> > > is enough. Adding them to a TOC won't hurt.
> >
> > One thing I kind of dislike about these magic mechanisms is that we end
> > up with a single, essentially unsorted list of stuff that readers have
> > to go digging their way through.
Heh, perhaps you're referring to the TOC tree. You'll see it
unsorted on diffs because the TOC tree there is hidden. As it is
a set, currently it will output a different result on each run.
Not a problem for users, but it makes harder to check differences
after patches, so I'm planning to submit a patch to keep it sorted
just for the sake of doing:
diff -u before/ after/
Thanks,
Mauro
^ permalink raw reply
* Re: [PATCH v12 12/22] gpu: nova-core: mm: Add page table entry operation traits
From: Alexandre Courbot @ 2026-05-04 14:31 UTC (permalink / raw)
To: Joel Fernandes
Cc: linux-kernel, Miguel Ojeda, Boqun Feng, Gary Guo, Bjorn Roy Baron,
Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
Danilo Krummrich, Dave Airlie, Daniel Almeida, dri-devel,
rust-for-linux, nova-gpu, Nikola Djukic, David Airlie, Boqun Feng,
John Hubbard, Alistair Popple, Timur Tabi, Edwin Peer,
Andrea Righi, Andy Ritger, Zhi Wang, Balbir Singh,
Philipp Stanner, alexeyi, Eliot Courtney, joel, linux-doc
In-Reply-To: <53c88538-dca7-41f1-bb2b-856035728c69@nvidia.com>
On Sun May 3, 2026 at 4:19 AM JST, Joel Fernandes wrote:
>> Please reorder things so they land, as much as possible, in their final
>> form. In this case this probably means defining the trait *before* the V2
>> and V3 page table definitions, so they can implement it from the get-go.
>
> That is a reasonable approach too, I can try to do that, but it is
> misleading to say '270 lines of diff that reviewers will have processed for
> nothing' which is nothing but fiction. Please look more carefully, the
> patch is iterative on the series.
For context, here is where the 270 lines of diff come from:
drivers/gpu/nova-core/mm/pagetable/ver2.rs | 150 ++++++++------
drivers/gpu/nova-core/mm/pagetable/ver3.rs | 120 +++++++----
But the number is not important. My feedback was that refactoring code
right after you introduce it is a strong indicator that something should
be reordered/squashed to prevent unneeded cognitive load on reviewers.
This is particularly important on long series like this one.
^ permalink raw reply
* Re: [PATCH v12 09/22] gpu: nova-core: mm: Add common types for all page table formats
From: Alexandre Courbot @ 2026-05-04 14:27 UTC (permalink / raw)
To: Joel Fernandes
Cc: linux-kernel, Miguel Ojeda, Boqun Feng, Gary Guo, Bjorn Roy Baron,
Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
Danilo Krummrich, Dave Airlie, Daniel Almeida, dri-devel,
rust-for-linux, nova-gpu, Nikola Djukic, David Airlie, Boqun Feng,
John Hubbard, Alistair Popple, Timur Tabi, Edwin Peer,
Andrea Righi, Andy Ritger, Zhi Wang, Balbir Singh,
Philipp Stanner, alexeyi, Eliot Courtney, joel, linux-doc
In-Reply-To: <a9d1fc72-a5aa-440e-88cf-b04882c24878@nvidia.com>
On Sun May 3, 2026 at 2:55 AM JST, Joel Fernandes wrote:
>
>
> On 5/2/2026 11:42 AM, Alexandre Courbot wrote:
>> On Sun Apr 26, 2026 at 6:14 AM JST, Joel Fernandes wrote:
>>> Add common page table types shared between MMU v2 and v3. These types
>>> are hardware-agnostic and used by both MMU versions.
>>>
>>> Cc: Nikola Djukic <ndjukic@nvidia.com>
>>> Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
>>> ---
>>> drivers/gpu/nova-core/mm.rs | 1 +
>>> drivers/gpu/nova-core/mm/pagetable.rs | 157 ++++++++++++++++++++++++++
>>> 2 files changed, 158 insertions(+)
>>> create mode 100644 drivers/gpu/nova-core/mm/pagetable.rs
>>>
>>> diff --git a/drivers/gpu/nova-core/mm.rs b/drivers/gpu/nova-core/mm.rs
>>> index 8b8a86980bb6..045e35c92b78 100644
>>> --- a/drivers/gpu/nova-core/mm.rs
>>> +++ b/drivers/gpu/nova-core/mm.rs
>>> @@ -32,6 +32,7 @@ macro_rules! impl_pfn_bounded {
>>> };
>>> }
>>>
>>> +pub(super) mod pagetable;
>>> pub(crate) mod pramin;
>>> pub(super) mod tlb;
>>>
>>> diff --git a/drivers/gpu/nova-core/mm/pagetable.rs b/drivers/gpu/nova-core/mm/pagetable.rs
>>> new file mode 100644
>>> index 000000000000..637ff43ea83a
>>> --- /dev/null
>>> +++ b/drivers/gpu/nova-core/mm/pagetable.rs
>>> @@ -0,0 +1,157 @@
>>> +// SPDX-License-Identifier: GPL-2.0
>>> +
>>> +//! Common page table types shared between MMU v2 and v3.
>>> +//!
>>> +//! This module provides foundational types used by both MMU versions:
>>> +//! - Page table level hierarchy
>>> +//! - Memory aperture types for PDEs and PTEs
>>> +
>>> +#![expect(dead_code)]
>>> +
>>> +use kernel::num::Bounded;
>>> +
>>> +use crate::gpu::Architecture;
>>> +
>>> +/// Extracts the page table index at a given level from a virtual address.
>>> +pub(super) trait VaLevelIndex {
>>> + /// Return the page table index at `level` for this virtual address.
>>> + fn level_index(&self, level: u64) -> u64;
>>> +}
>>> +
>>> +/// MMU version enumeration.
>>> +#[derive(Debug, Clone, Copy, PartialEq, Eq)]
>>> +pub(crate) enum MmuVersion {
>>> + /// MMU v2 for Turing/Ampere/Ada.
>>> + V2,
>>> + /// MMU v3 for Hopper and later.
>>> + V3,
>>> +}
>>> +
>>> +impl From<Architecture> for MmuVersion {
>>> + fn from(arch: Architecture) -> Self {
>>> + match arch {
>>> + Architecture::Turing | Architecture::Ampere | Architecture::Ada => Self::V2,
>>> + // In the future, uncomment the following to support V3.
>>> + // _ => Self::V3,
>>
>> The architecture definitions for Blackwell are now in `drm-rust-next`,
>> so I think the next iteration can handle this.
>>
>> Which reminds me: is V3 working? I remember some fixup by Eliot
>> wandering around, have you integrated it to the series?
>
> MMU v3 is working. But this series needs more patches on top of it for
> Blackwell, I am carrying those extras in a different tree based on this
> series. The fixes from have all been integrated (either in this series or
> that other tree of extras).
>
> Have all the base blackwell patches made it into drm-rust-next? If yes, I
> can rebase this series on it, pull in the extras, and continue posting the
> next mm iteration with blackwell support.
The architecture definitions are there yes. The full support (as in GSP
booting), not yet - `Gsp::boot` returns `ENOTSUPP` at the moment. If
practical, including support would be nice as it would allow the
self-tests to run on Blackwell as soon as the series is merged.
^ permalink raw reply
* Re: [PATCH v2 0/5] Cleanup Linux Watchdog documentation
From: Guenter Roeck @ 2026-05-04 14:11 UTC (permalink / raw)
To: Philipp Hahn, Wim Van Sebroeck
Cc: Philipp Hahn, linux-watchdog, linux-doc, linux-kernel
In-Reply-To: <cover.1777902230.git.phahn-oss@avm.de>
On 5/4/26 06:49, Philipp Hahn wrote:
> From: Philipp Hahn <phahn-oss@avm.de>
>
> Hello,
>
> while reading the documentation on the Linux kernel watchdog
> subsystem[1] I noticed some strange looking formatting: `struct`s are
> detected automatically by Sphinx and the word **after** it is considered
> the name of the structure, but the watchdog documentation puts the name
> **before** the word `struct`; this leads to the wrong word being
> emphasized.
>
> Also reorganize the index page to better separate the documentation on
> generic infrastructure from specific drivers – which are mostly obsolete
> and relevant for historic documentation only.
>
> Please apply; thank you for all your work.
>
> Philipp
>
> [1]: https://docs.kernel.org/watchdog/convert_drivers_to_kernel_api.html
From Documentation/process/submitting-patches.rst:
However, for a multi-patch series, it is generally
best to avoid using In-Reply-To: to link to older versions of the
series. This way multiple versions of the patch don't become an
unmanageable forest of references in email clients.
... but you did that anyway. It also almost guarantees that new
versions of a patch series get lost.
Guenter
^ permalink raw reply
* Re: [PATCH 6/6] watchdog: Prefix WDT with ICS for clarity
From: Guenter Roeck @ 2026-05-04 14:00 UTC (permalink / raw)
To: Philipp Hahn, Wim Van Sebroeck
Cc: Philipp Hahn, linux-watchdog, linux-doc, linux-kernel
In-Reply-To: <a0a05dca5531c6a5ce0cc772dfa0c0522538e23f.1777892508.git.phahn-oss@avm.de>
On 5/4/26 04:02, Philipp Hahn wrote:
> From: Philipp Hahn <phahn-oss@avm.de>
>
> `wdt.rst` is only about the Watchdog timer from "Industrial Computer
> Source" (ICS). Change the title to better express this.
>
Please also explain "ICS" into the documentation file.
We can not rename the Kconfig symbol, but I'd also suggest to rename
the documentation to icswdt.rst or maybe better watchdog-ics.rst.
Additional comment below.
Thanks,
Guenter
> Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
> ---
> Documentation/watchdog/wdt.rst | 6 +++---
> drivers/watchdog/Kconfig | 6 +++---
> 2 files changed, 6 insertions(+), 6 deletions(-)
>
> diff --git a/Documentation/watchdog/wdt.rst b/Documentation/watchdog/wdt.rst
> index d97b0361535b0..be8cc0b9f31e1 100644
> --- a/Documentation/watchdog/wdt.rst
> +++ b/Documentation/watchdog/wdt.rst
> @@ -1,6 +1,6 @@
> -============================================================
> -WDT Watchdog Timer Interfaces For The Linux Operating System
> -============================================================
> +================================================================
> +ICS WDT Watchdog Timer Interfaces For The Linux Operating System
> +================================================================
>
> Last Reviewed: 10/05/2007
>
> diff --git a/drivers/watchdog/Kconfig b/drivers/watchdog/Kconfig
> index dc78729ba2a5d..2cbf0391a065a 100644
> --- a/drivers/watchdog/Kconfig
> +++ b/drivers/watchdog/Kconfig
> @@ -2270,10 +2270,10 @@ config MIXCOMWD
> Most people will say N.
>
> config WDT
> - tristate "WDT Watchdog timer"
> + tristate "ICS WDT Watchdog timer"
WDT is now redundant. Maybe "ICS WDT500P/WDT501P Watchdog timer" to
match the PCI variant.
> depends on ISA
> help
> - If you have a WDT500P or WDT501P watchdog board, say Y here,
> + If you have an ICS WDT500P or WDT501P watchdog board, say Y here,
> otherwise N. It is not possible to probe for this board, which means
> that you have to inform the kernel about the IO port and IRQ that
> is needed (you can do this via the io and irq parameters)
> @@ -2304,7 +2304,7 @@ config PCIPCWATCHDOG
> Most people will say N.
>
> config WDTPCI
> - tristate "PCI-WDT500/501 Watchdog timer"
> + tristate "ICS PCI-WDT500/501 Watchdog timer"
> depends on PCI && HAS_IOPORT
> help
> If you have a PCI-WDT500/501 watchdog board, say Y here, otherwise N.
^ permalink raw reply
* [PATCH v2 3/5] watchdog: Replace intermixed tab/space indent
From: Philipp Hahn @ 2026-05-04 13:50 UTC (permalink / raw)
To: Wim Van Sebroeck, Guenter Roeck
Cc: Philipp Hahn, linux-watchdog, linux-doc, linux-kernel
In-Reply-To: <cover.1777902230.git.phahn-oss@avm.de>
From: Philipp Hahn <phahn-oss@avm.de>
Consistently indent all lines with tabs instead of having one line using
blanks.
Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
---
Documentation/watchdog/watchdog-api.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Documentation/watchdog/watchdog-api.rst b/Documentation/watchdog/watchdog-api.rst
index 2c138eaa43397..78d0940155b13 100644
--- a/Documentation/watchdog/watchdog-api.rst
+++ b/Documentation/watchdog/watchdog-api.rst
@@ -161,7 +161,7 @@ available to ask what the device can do::
the fields returned in the struct watchdog_info are:
================ =============================================
- identity a string identifying the watchdog driver
+ identity a string identifying the watchdog driver
firmware_version the firmware version of the card if available
options a flags describing what the device supports
================ =============================================
--
2.43.0
^ permalink raw reply related
* [PATCH v2 1/5] watchdog: Change suffix .txt to .rst in references
From: Philipp Hahn @ 2026-05-04 13:49 UTC (permalink / raw)
To: Wim Van Sebroeck, Guenter Roeck
Cc: Philipp Hahn, linux-watchdog, linux-doc, linux-kernel
In-Reply-To: <cover.1777902230.git.phahn-oss@avm.de>
From: Philipp Hahn <phahn-oss@avm.de>
Fix link to documentation, which has already been converted to reST.
Also remove apostrophes which are no longer needed.
Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
---
Documentation/watchdog/convert_drivers_to_kernel_api.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Documentation/watchdog/convert_drivers_to_kernel_api.rst b/Documentation/watchdog/convert_drivers_to_kernel_api.rst
index e83609a5d0071..9eddb962f8e4c 100644
--- a/Documentation/watchdog/convert_drivers_to_kernel_api.rst
+++ b/Documentation/watchdog/convert_drivers_to_kernel_api.rst
@@ -114,7 +114,7 @@ Add the watchdog operations
---------------------------
All possible callbacks are defined in 'struct watchdog_ops'. You can find it
-explained in 'watchdog-kernel-api.txt' in this directory. start() and
+explained in watchdog-kernel-api.rst in this directory. start() and
owner must be set, the rest are optional. You will easily find corresponding
functions in the old driver. Note that you will now get a pointer to the
watchdog_device as a parameter to these functions, so you probably have to
@@ -153,7 +153,7 @@ Add the watchdog device
Now we need to create a 'struct watchdog_device' and populate it with the
necessary information for the framework. The struct is also explained in detail
-in 'watchdog-kernel-api.txt' in this directory. We pass it the mandatory
+in watchdog-kernel-api.rst in this directory. We pass it the mandatory
watchdog_info struct and the newly created watchdog_ops. Often, old drivers
have their own record-keeping for things like bootstatus and timeout using
static variables. Those have to be converted to use the members in
--
2.43.0
^ permalink raw reply related
* [PATCH v2 5/5] watchdog: Prefix WDT with ICS for clarity
From: Philipp Hahn @ 2026-05-04 13:50 UTC (permalink / raw)
To: Wim Van Sebroeck, Guenter Roeck
Cc: Philipp Hahn, linux-watchdog, linux-doc, linux-kernel
In-Reply-To: <cover.1777902230.git.phahn-oss@avm.de>
From: Philipp Hahn <phahn-oss@avm.de>
`wdt.rst` is only about the Watchdog timer from "Industrial Computer
Source" (ICS). Change the title to better express this.
Add missing SPDX license identifier `GPL-2.0-or-later` same as code to
silence `checkpatch`.
Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
---
Documentation/watchdog/wdt.rst | 8 +++++---
drivers/watchdog/Kconfig | 6 +++---
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/Documentation/watchdog/wdt.rst b/Documentation/watchdog/wdt.rst
index d97b0361535b0..5be200e08d02c 100644
--- a/Documentation/watchdog/wdt.rst
+++ b/Documentation/watchdog/wdt.rst
@@ -1,6 +1,8 @@
-============================================================
-WDT Watchdog Timer Interfaces For The Linux Operating System
-============================================================
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+================================================================
+ICS WDT Watchdog Timer Interfaces For The Linux Operating System
+================================================================
Last Reviewed: 10/05/2007
diff --git a/drivers/watchdog/Kconfig b/drivers/watchdog/Kconfig
index dc78729ba2a5d..2cbf0391a065a 100644
--- a/drivers/watchdog/Kconfig
+++ b/drivers/watchdog/Kconfig
@@ -2270,10 +2270,10 @@ config MIXCOMWD
Most people will say N.
config WDT
- tristate "WDT Watchdog timer"
+ tristate "ICS WDT Watchdog timer"
depends on ISA
help
- If you have a WDT500P or WDT501P watchdog board, say Y here,
+ If you have an ICS WDT500P or WDT501P watchdog board, say Y here,
otherwise N. It is not possible to probe for this board, which means
that you have to inform the kernel about the IO port and IRQ that
is needed (you can do this via the io and irq parameters)
@@ -2304,7 +2304,7 @@ config PCIPCWATCHDOG
Most people will say N.
config WDTPCI
- tristate "PCI-WDT500/501 Watchdog timer"
+ tristate "ICS PCI-WDT500/501 Watchdog timer"
depends on PCI && HAS_IOPORT
help
If you have a PCI-WDT500/501 watchdog board, say Y here, otherwise N.
--
2.43.0
^ permalink raw reply related
* [PATCH v2 4/5] watchdog: Separate kind of documentation
From: Philipp Hahn @ 2026-05-04 13:50 UTC (permalink / raw)
To: Wim Van Sebroeck, Guenter Roeck
Cc: Philipp Hahn, linux-watchdog, linux-doc, linux-kernel
In-Reply-To: <cover.1777902230.git.phahn-oss@avm.de>
From: Philipp Hahn <phahn-oss@avm.de>
Currently there are several (sub-)documents for "Generic kernel
infrastructure API" and several "driver specific" documents. Put each
one into its own sub-section.
Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
---
Documentation/watchdog/index.rst | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/Documentation/watchdog/index.rst b/Documentation/watchdog/index.rst
index 1cea24681e6bd..8886910550344 100644
--- a/Documentation/watchdog/index.rst
+++ b/Documentation/watchdog/index.rst
@@ -4,15 +4,25 @@
Watchdog Support
================
+Kernel infrastructure
+=====================
+
.. toctree::
:maxdepth: 1
- hpwdt
- mlx-wdt
- pcwd-watchdog
watchdog-api
watchdog-kernel-api
- watchdog-parameters
watchdog-pm
- wdt
convert_drivers_to_kernel_api
+
+Driver specific
+===============
+
+.. toctree::
+ :maxdepth: 1
+
+ watchdog-parameters
+ wdt
+ hpwdt
+ mlx-wdt
+ pcwd-watchdog
--
2.43.0
^ permalink raw reply related
* [PATCH v2 2/5] watchdog: Move `struct` before name
From: Philipp Hahn @ 2026-05-04 13:50 UTC (permalink / raw)
To: Wim Van Sebroeck, Guenter Roeck
Cc: Philipp Hahn, linux-watchdog, linux-doc, linux-kernel
In-Reply-To: <cover.1777902230.git.phahn-oss@avm.de>
From: Philipp Hahn <phahn-oss@avm.de>
Write `struct ` before the structure name as Sphinx otherwise uses the
following word after it. See
https://docs.kernel.org/watchdog/watchdog-api.html#environmental-monitoring
Signed-off-by: Philipp Hahn <phahn-oss@avm.de>
---
.../convert_drivers_to_kernel_api.rst | 24 +++++++++----------
Documentation/watchdog/watchdog-api.rst | 2 +-
.../watchdog/watchdog-kernel-api.rst | 2 +-
3 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/Documentation/watchdog/convert_drivers_to_kernel_api.rst b/Documentation/watchdog/convert_drivers_to_kernel_api.rst
index 9eddb962f8e4c..004fe6ef76e94 100644
--- a/Documentation/watchdog/convert_drivers_to_kernel_api.rst
+++ b/Documentation/watchdog/convert_drivers_to_kernel_api.rst
@@ -11,12 +11,12 @@ This document shall guide you for this task. The necessary steps are described
as well as things to look out for.
-Remove the file_operations struct
+Remove the struct file_operations
---------------------------------
Old drivers define their own file_operations for actions like open(), write(),
etc... These are now handled by the framework and just call the driver when
-needed. So, in general, the 'file_operations' struct and assorted functions can
+needed. So, in general, the struct file_operations and assorted functions can
go. Only very few driver-specific details have to be moved to other functions.
Here is a overview of the functions and probably needed actions:
@@ -36,7 +36,7 @@ Here is a overview of the functions and probably needed actions:
from the driver:
WDIOC_GETSUPPORT:
- Returns the mandatory watchdog_info struct from the driver
+ Returns the mandatory struct watchdog_info from the driver
WDIOC_GETSTATUS:
Needs the status-callback defined, otherwise returns 0
@@ -88,8 +88,8 @@ refactoring. The rest can go.
Remove the miscdevice
---------------------
-Since the file_operations are gone now, you can also remove the 'struct
-miscdevice'. The framework will create it on watchdog_dev_register() called by
+Since the file_operations are gone now, you can also remove the struct
+miscdevice. The framework will create it on watchdog_dev_register() called by
watchdog_register_device()::
-static struct miscdevice s3c2410wdt_miscdev = {
@@ -113,10 +113,10 @@ them. Includes can be removed, too. For example::
Add the watchdog operations
---------------------------
-All possible callbacks are defined in 'struct watchdog_ops'. You can find it
+All possible callbacks are defined in struct watchdog_ops. You can find it
explained in watchdog-kernel-api.rst in this directory. start() and
owner must be set, the rest are optional. You will easily find corresponding
-functions in the old driver. Note that you will now get a pointer to the
+functions in the old driver. Note that you will now get a pointer to the struct
watchdog_device as a parameter to these functions, so you probably have to
change the function header. Other changes are most likely not needed, because
here simply happens the direct hardware access. If you have device-specific
@@ -151,12 +151,12 @@ A typical function-header change looks like::
Add the watchdog device
-----------------------
-Now we need to create a 'struct watchdog_device' and populate it with the
-necessary information for the framework. The struct is also explained in detail
+Now we need to create a struct watchdog_device and populate it with the
+necessary information for the framework. The structure is also explained in detail
in watchdog-kernel-api.rst in this directory. We pass it the mandatory
-watchdog_info struct and the newly created watchdog_ops. Often, old drivers
+struct watchdog_info and the newly created struct watchdog_ops. Often, old drivers
have their own record-keeping for things like bootstatus and timeout using
-static variables. Those have to be converted to use the members in
+static variables. Those have to be converted to use the members in struct
watchdog_device. Note that the timeout values are unsigned int. Some drivers
use signed int, so this has to be converted, too.
@@ -174,7 +174,7 @@ Handle the 'nowayout' feature
A few drivers use nowayout statically, i.e. there is no module parameter for it
and only CONFIG_WATCHDOG_NOWAYOUT determines if the feature is going to be
used. This needs to be converted by initializing the status variable of the
-watchdog_device like this::
+struct watchdog_device like this::
.status = WATCHDOG_NOWAYOUT_INIT_STATUS,
diff --git a/Documentation/watchdog/watchdog-api.rst b/Documentation/watchdog/watchdog-api.rst
index 78e228c272cf8..2c138eaa43397 100644
--- a/Documentation/watchdog/watchdog-api.rst
+++ b/Documentation/watchdog/watchdog-api.rst
@@ -158,7 +158,7 @@ available to ask what the device can do::
struct watchdog_info ident;
ioctl(fd, WDIOC_GETSUPPORT, &ident);
-the fields returned in the ident struct are:
+the fields returned in the struct watchdog_info are:
================ =============================================
identity a string identifying the watchdog driver
diff --git a/Documentation/watchdog/watchdog-kernel-api.rst b/Documentation/watchdog/watchdog-kernel-api.rst
index 5649c54cf6fbe..e2c1386c95509 100644
--- a/Documentation/watchdog/watchdog-kernel-api.rst
+++ b/Documentation/watchdog/watchdog-kernel-api.rst
@@ -257,7 +257,7 @@ bit-operations. The status bits that are defined are:
To set the WDOG_NO_WAY_OUT status bit (before registering your watchdog
timer device) you can either:
- * set it statically in your watchdog_device struct with
+ * set it statically in your struct watchdog_device with
.status = WATCHDOG_NOWAYOUT_INIT_STATUS,
--
2.43.0
^ permalink raw reply related
* [PATCH v2 0/5] Cleanup Linux Watchdog documentation
From: Philipp Hahn @ 2026-05-04 13:49 UTC (permalink / raw)
To: Wim Van Sebroeck, Guenter Roeck
Cc: Philipp Hahn, linux-watchdog, linux-doc, linux-kernel
In-Reply-To: <cover.1777892508.git.phahn-oss@avm.de>
From: Philipp Hahn <phahn-oss@avm.de>
Hello,
while reading the documentation on the Linux kernel watchdog
subsystem[1] I noticed some strange looking formatting: `struct`s are
detected automatically by Sphinx and the word **after** it is considered
the name of the structure, but the watchdog documentation puts the name
**before** the word `struct`; this leads to the wrong word being
emphasized.
Also reorganize the index page to better separate the documentation on
generic infrastructure from specific drivers – which are mostly obsolete
and relevant for historic documentation only.
Please apply; thank you for all your work.
Philipp
[1]: https://docs.kernel.org/watchdog/convert_drivers_to_kernel_api.html
---
Philipp Hahn (5):
watchdog: Change suffix .txt to .rst in references
watchdog: Move `struct` before name
watchdog: Replace intermixed tab/space indent
watchdog: Separate kind of documentation
watchdog: Prefix WDT with ICS for clarity
v1 -> 2
- Drop invalid SPDX change to Documentation/watchdog/index.rst
- Add SPDX to Documentation/watchdog/wdt.rst
.../convert_drivers_to_kernel_api.rst | 28 +++++++++----------
Documentation/watchdog/index.rst | 20 +++++++++----
Documentation/watchdog/watchdog-api.rst | 4 +--
.../watchdog/watchdog-kernel-api.rst | 2 +-
Documentation/watchdog/wdt.rst | 8 ++++--
drivers/watchdog/Kconfig | 6 ++--
6 files changed, 40 insertions(+), 28 deletions(-)
--
2.43.0
^ permalink raw reply
* Re: [PATCH net-next v2 1/2] dpll: move fractional-frequency-offset-ppt under pin-parent-device
From: Ivan Vecera @ 2026-05-04 12:59 UTC (permalink / raw)
To: Jiri Pirko
Cc: netdev, Andrew Lunn, Arkadiusz Kubalewski, David S. Miller,
Donald Hunter, Eric Dumazet, Jakub Kicinski, Jonathan Corbet,
Leon Romanovsky, Mark Bloch, Michal Schmidt, Paolo Abeni,
Pasi Vaananen, Petr Oros, Prathosh Satish, Saeed Mahameed,
Shuah Khan, Simon Horman, Tariq Toukan, Vadim Fedorenko,
linux-doc, linux-kernel, linux-rdma
In-Reply-To: <afiQTPaMyAApbLRk@FV6GYCPJ69>
On 5/4/26 2:26 PM, Jiri Pirko wrote:
> Mon, May 04, 2026 at 11:36:19AM +0200, ivecera@redhat.com wrote:
>> Hi Jiri,
>>
>> On 5/4/26 10:48 AM, Jiri Pirko wrote:
>>> Thu, Apr 30, 2026 at 07:36:10PM +0200, ivecera@redhat.com wrote:
>>>> Move the fractional-frequency-offset-ppt attribute from the top-level
>>>> pin attributes into the pin-parent-device nested attribute set. This
>>>> makes it consistent with phase-offset which is already per-parent and
>>>> clarifies that FFO PPT represents the frequency difference between
>>>> a pin and its parent DPLL device.
>>>>
>>>> The top-level fractional-frequency-offset attribute (in PPM) remains
>>>> unchanged for backward compatibility.
>>>
>>> That is odd. The ppt one was added just for higher precision but was
>>> semantically the same. Now you change it. Could you still treat both the
>>> same?
>>>
>> WDYM?
>>
>> Keep fractional-frequency-offset-ppt at the top-level and add both
>> fractional-frequency-offset and fractional-frequency-offset-ppt into
>> pin-parent-device nested attribute set?
>
> Since both are the same, only different unit, it would make sense to
> treat them both the same. That prevents from user confusion, hopefully.
>
Agree... will update in v3.
Thanks,
Ivan
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox