* Re: [PATCH v19 19/40] dept: apply timeout consideration to hashed-waitqueue wait
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: ojeda, linux-ide, linux-kernel, Anna Schumaker, Chuck Lever,
dlemoal, linux-rt-devel, Jeff Layton, tytso, cassel,
linux-modules, dri-devel
In-Reply-To: <20260706061928.66713-20-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Missing `sdt_might_sleep_end()` cleanup in the error path of wait macros (e.g., `___wait_var_event`, `___wait_event`, `___swait_event`) bypasses `dept_clean_stage()`, leaking the staged wait event state and corrupting DEPT tracking.
--
commit cf04720896ab037609320947c88e8faa57dbddd7
Author: Byungchul Park <byungchul@sk.com>
dept: apply timeout consideration to hashed-waitqueue wait
Now that CONFIG_DEPT_AGGRESSIVE_TIMEOUT_WAIT was introduced, apply the
consideration to hashed-waitqueue wait, assuming an input 'ret' in
___wait_var_event() macro is used as a timeout value.
> diff --git a/include/linux/wait_bit.h b/include/linux/wait_bit.h
> index 179a616ad245a..9885ac4e1ded5 100644
> --- a/include/linux/wait_bit.h
> +++ b/include/linux/wait_bit.h
> @@ -258,7 +258,7 @@ extern wait_queue_head_t *__var_waitqueue(void *p);
> struct wait_bit_queue_entry __wbq_entry; \
> long __ret = ret; /* explicit shadow */ \
> \
> - sdt_might_sleep_start(NULL); \
> + sdt_might_sleep_start_timeout(NULL, __ret); \
[Severity: High]
Does this error path properly clean up the dept wait state?
Lower in the ___wait_var_event() macro, if a signal is received it jumps to
__out:
if (___wait_is_interruptible(state) && __int) { \
__ret = __int; \
goto __out; \
} \
...
sdt_might_sleep_end(); \
__out: __ret; \
Because the goto __out branch skips sdt_might_sleep_end(), does it leave a
dangling stage context in the dept subsystem's tracking graph?
If the stage context remains populated, could a subsequent wait on the same
task be incorrectly seen as a nested stage, potentially corrupting dept
tracking or leading to false deadlock reports?
(Note: A similar goto __out pattern skipping sdt_might_sleep_end() also
exists in ___wait_event() in include/linux/wait.h and ___swait_event() in
include/linux/swait.h).
> init_wait_var_entry(&__wbq_entry, var, \
> exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
> for (;;) { \
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=19
^ permalink raw reply
* Re: [PATCH v19 18/40] dept: apply timeout consideration to waitqueue wait
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: cassel, tytso, ojeda, linux-kernel, dlemoal, Jeff Layton,
linux-rt-devel, Anna Schumaker, linux-modules, dri-devel,
Chuck Lever, linux-ide
In-Reply-To: <20260706061928.66713-19-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] The timeout consideration logic fails for wait_event_hrtimeout(), hashed-waitqueues, and DMA fences, causing them to be falsely treated as infinite waits.
- [Medium] The ___wait_event() and ___wait_var_event() macros skip the DEPT sleep end annotation on the signal interruption error path, leaking the DEPT tracking stage context.
--
commit 25089e0217c82211740316928fec38c6d2b7e4fa
Author: Byungchul Park <byungchul@sk.com>
dept: apply timeout consideration to waitqueue wait
This commit applies timeout consideration to waitqueue waits by using the
ret parameter in ___wait_event() as a timeout value for DEPT tracking. It
leverages CONFIG_DEPT_AGGRESSIVE_TIMEOUT_WAIT to help reduce false positive
deadlock reports for waits that are meant to expire.
> diff --git a/include/linux/wait.h b/include/linux/wait.h
> index c8f8b44060fb9..a9524bc8630b7 100644
> --- a/include/linux/wait.h
> +++ b/include/linux/wait.h
> @@ -306,7 +306,7 @@ extern void init_wait_entry(struct wait_queue_entry *wq_entry, int flags);
> struct wait_queue_entry __wq_entry; \
> long __ret = ret; /* explicit shadow */ \
> \
> - sdt_might_sleep_start(NULL); \
> + sdt_might_sleep_start_timeout(NULL, __ret); \
[Severity: Medium]
Does this logic also need to be applied to __wait_event_hrtimeout(),
hashed-waitqueues, and DMA fences?
For example, __wait_event_hrtimeout() passes 0 as the 'ret' parameter to
___wait_event(), causing sdt_might_sleep_start_timeout() to treat it as an
infinite wait.
Additionally, hashed-waitqueues (in ___wait_var_event) and DMA fences
(in dma_fence_default_wait and dma_fence_wait_any_timeout) still hardcode
sdt_might_sleep_start(NULL) even when taking a timeout. This seems to cause
DEPT to falsely report deadlocks for these bounded waits.
> init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
> for (;;) { \
> long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
[Severity: Medium]
Can the DEPT sleep end annotation be skipped on the signal interruption
error path?
Looking further down in the ___wait_event() macro, if the wait is
interruptible and interrupted by a signal, it takes the goto __out branch:
if (___wait_is_interruptible(state) && __int) {
__ret = __int;
goto __out;
}
...
}
finish_wait(&wq_head, &__wq_entry);
sdt_might_sleep_end();
__out: __ret;
This skips over sdt_might_sleep_end(), which leaks the DEPT wait stage
context. As a result, DEPT will mistakenly treat subsequent wait events for
this task as nested waits.
The same issue appears to exist in ___wait_var_event() for
hashed-waitqueues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=18
^ permalink raw reply
* Re: [PATCH v19 07/40] dept: distinguish each work from another
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: linux-rt-devel, cassel, linux-ide, tytso, linux-kernel,
Anna Schumaker, linux-modules, Chuck Lever, ojeda, dlemoal,
dri-devel, Jeff Layton
In-Reply-To: <20260706061928.66713-8-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] `dept_update_cxt()` hardcodes the process context ID, failing to track bottom-half (BH) work items in softirq context and corrupting the context of preempted tasks.
--
commit b8dda7d95e5377770d28c6712cf8fe7fafa98ade
Author: Byungchul Park <byungchul@sk.com>
dept: distinguish each work from another
This commit updates DEPT to assign a different context ID to each work item.
Since workqueues provide their own concurrency control, wait operations in a
work item do not prevent events in other works from executing.
> diff --git a/kernel/workqueue.c b/kernel/workqueue.c
> index c6ea96d5b7167..4a4075d0697c7 100644
> --- a/kernel/workqueue.c
> +++ b/kernel/workqueue.c
[ ... ]
> @@ -3204,6 +3205,8 @@ __acquires(&pool->lock)
>
> lockdep_copy_map(&lockdep_map, &work->lockdep_map);
> #endif
> + dept_update_cxt();
> +
[Severity: Medium]
Will this unconditionally modify the process context ID, even when tracking
bottom-half (BH) work items running in softirq context?
When a BH workqueue executes multiple work items in a single softirq batch,
process_one_work() executes them and calls dept_update_cxt(). However,
dept_update_cxt() is implemented as:
kernel/dependency/dept.c:dept_update_cxt() {
struct dept_task *dt = dept_task();
dt->cxt_id[DEPT_CXT_PROCESS] += 1UL << DEPT_CXTS_NR;
}
This hardcodes the increment to DEPT_CXT_PROCESS rather than checking
cur_cxt() to increment DEPT_CXT_SIRQ. Does this cause DEPT to fail in
distinguishing sequential BH work items and falsely report dependencies?
Also, can this corrupt the continuous dependency tracking context of
whatever task was preempted by the softirq?
> /* ensure we're on the correct CPU */
> WARN_ON_ONCE(!(pool->flags & POOL_DISASSOCIATED) &&
> raw_smp_processor_id() != pool->cpu);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=7
^ permalink raw reply
* Re: [PATCH v19 06/40] dept: distinguish each kernel context from another
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: cassel, dri-devel, Anna Schumaker, ojeda, tytso, linux-modules,
Jeff Layton, Chuck Lever, dlemoal, linux-ide, linux-kernel,
linux-rt-devel
In-Reply-To: <20260706061928.66713-7-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] cur_cxt() incorrectly identifies a hardirq interrupting a softirq as a softirq context, mixing dependencies between unrelated execution contexts.
--
commit 504ac1eb7d0a4081c645648037ec455b5ec741d7
Author: Byungchul Park <byungchul@sk.com>
dept: distinguish each kernel context from another
Each unique kernel context, in dept's point of view, should be
identified on every entrance to kernel mode e.g. system call or user
oriented fault. Otherwise, dept may track meaningless dependencies
across different kernel context.
> diff --git a/kernel/dependency/dept.c b/kernel/dependency/dept.c
> index fd091ae28ae27..1c4de1b55730a 100644
> --- a/kernel/dependency/dept.c
> +++ b/kernel/dependency/dept.c
[ ... ]
> -static int cur_irq(void)
> +static int cur_cxt(void)
> {
> if (lockdep_softirq_context(current))
> - return DEPT_SIRQ;
> + return DEPT_CXT_SIRQ;
> if (lockdep_hardirq_context())
> - return DEPT_HIRQ;
> - return DEPT_IRQS_NR;
> + return DEPT_CXT_HIRQ;
> + return DEPT_CXT_PROCESS;
> }
[Severity: High]
This is a pre-existing issue, but does cur_cxt() incorrectly prioritize softirq
over hardirq context here?
If a softirq is executing and is interrupted by a hardirq, both
lockdep_softirq_context(current) and lockdep_hardirq_context() evaluate to
true. Because the softirq check comes first, cur_cxt() will return
DEPT_CXT_SIRQ instead of DEPT_CXT_HIRQ.
Could this cause the waits and events triggered inside the interrupting hardirq
to be erroneously attributed to the interrupted softirq's context, potentially
mixing unrelated dependencies and leading to false positive circular dependency
reports?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=6
^ permalink raw reply
* Re: [PATCH v19 15/40] dept: track timeout waits separately with a new Kconfig
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: cassel, linux-rt-devel, ojeda, linux-ide, dlemoal, Chuck Lever,
Jeff Layton, linux-modules, Anna Schumaker, linux-kernel,
dri-devel, tytso
In-Reply-To: <20260706061928.66713-16-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Semantic collision in the timeoutval argument causes 0-timeout try-waits to be incorrectly tracked as hard blocking dependencies.
- [High] Enabling CONFIG_DEPT_AGGRESSIVE_TIMEOUT_WAIT masks real deadlocks because a timeout wait permanently poisons the dependency edge, causing subsequent strict (non-timeout) waits to be ignored.
--
commit 1b06fc68b17e6c7948f8187e55952d1767e48762
Author: Byungchul Park <byungchul@sk.com>
dept: track timeout waits separately with a new Kconfig
This patch modifies DEPT to track timeout waits separately to avoid
reporting false positive deadlocks for deliberate timeouts. It also
introduces the CONFIG_DEPT_AGGRESSIVE_TIMEOUT_WAIT option to toggle this
tracking behavior.
> diff --git a/kernel/dependency/dept.c b/kernel/dependency/dept.c
> index 3af360ba17d8e..5ef85977e6314 100644
> --- a/kernel/dependency/dept.c
> +++ b/kernel/dependency/dept.c
[ ... ]
> @@ -810,6 +812,24 @@ static void print_dep(struct dept_dep *d)
>
> static void save_current_stack(int skip);
>
> +static bool is_timeout_wait_circle(struct dept_class *c)
> +{
> + struct dept_class *fc = c->bfs_parent;
> + struct dept_class *tc = c;
> +
> + do {
> + struct dept_dep *d = lookup_dep(fc, tc);
> +
> + if (d->wait->timeout)
> + return true;
[Severity: High]
If CONFIG_DEPT_AGGRESSIVE_TIMEOUT_WAIT is enabled, does this mask genuine
deadlocks if a strict wait follows a timeout wait?
If a timeout wait between class A and class B occurs first, it creates a
dependency edge with wait->timeout set to true. If a strict (non-timeout)
wait subsequently occurs between the same classes, add_dep() finds the
existing edge and returns early:
kernel/dependency/dept.c:add_dep() {
if (lookup_dep(fc, tc))
return;
...
}
The edge is never upgraded to reflect the stricter dependency.
If a deadlock later happens involving this edge, would is_timeout_wait_circle()
see the stale timeout flag and incorrectly report it as an intended circular
dependency instead of a genuine deadlock?
> +
> + tc = fc;
> + fc = fc->bfs_parent;
> + } while (tc != c);
> +
> + return false;
> +}
[ ... ]
> @@ -2355,14 +2381,23 @@ static void __dept_event(struct dept_map *m, struct dept_map *real_m,
> }
>
> void dept_wait(struct dept_map *m, unsigned long w_f,
> - unsigned long ip, const char *w_fn, int sub_l)
> + unsigned long ip, const char *w_fn, int sub_l,
> + long timeoutval)
> {
> struct dept_task *dt = dept_task();
> unsigned long flags;
> + bool timeout;
>
> if (unlikely(!dept_working()))
> return;
>
> + timeout = timeoutval > 0 && timeoutval < MAX_SCHEDULE_TIMEOUT;
[Severity: High]
Does this logic correctly handle try-waits or polling?
When a caller invokes a polling wait like wait_for_completion_timeout(x, 0),
the timeoutval is 0. Because 0 > 0 evaluates to false, this sets timeout to
false. DEPT then tracks this non-blocking poll as an indefinite hard wait.
Can this lead to false positive deadlock reports when no actual blocking
occurs?
> +
> +#if !defined(CONFIG_DEPT_AGGRESSIVE_TIMEOUT_WAIT)
> + if (timeout)
> + return;
> +#endif
> +
> if (dt->recursive)
> return;
>
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=15
^ permalink raw reply
* Re: [PATCH v19 02/40] dept: add single event dependency tracker APIs
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: Chuck Lever, Anna Schumaker, ojeda, linux-modules, Jeff Layton,
tytso, cassel, linux-rt-devel, dri-devel, dlemoal, linux-ide,
linux-kernel
In-Reply-To: <20260706061928.66713-3-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] Unsafe macro conversion of functions causes double-evaluation of arguments, leading to logic bugs and memory corruption.
--
commit e5795ffb367b1b68ef8574b0a67f0bf3654baf77
Author: Byungchul Park <byungchul@sk.com>
dept: add single event dependency tracker APIs
Wrapped the base APIs for easier annotation on wait and event. Start
with supporting waiters on each single event. More general support for
multiple events is a future work. Do more when the need arises.
> diff --git a/include/linux/dept_sdt.h b/include/linux/dept_sdt.h
> new file mode 100644
> index 0000000000000..0535f763b21be
> --- /dev/null
> +++ b/include/linux/dept_sdt.h
[ ... ]
> +#define sdt_map_init_key(m, k) dept_map_init(m, k, 0, #m)
> +
> +#define sdt_wait(m) \
> + do { \
> + dept_request_event(m); \
> + dept_wait(m, 1UL, _THIS_IP_, __func__, 0); \
> + } while (0)
[Severity: Critical]
Can this macro evaluate its argument multiple times, causing a regression
if the argument contains side effects?
Unlike sdt_might_sleep_start(m) later in this file which safely captures
its argument in a local variable __m, sdt_wait(m) evaluates its argument
twice.
If an expression with a post-increment is passed to a macro like this,
the side effect will execute multiple times. This is especially risky
if existing synchronization primitives like wait_for_completion(x) are
converted into similar macros to integrate with the tracker.
For example, looking at drivers/vdpa/mlx5/core/resources.c:
mlx5_vdpa_add_res() {
...
while (completed < issued)
wait_for_completion(&cmds[completed++].cmd_done);
...
}
If wait_for_completion() or a similar wrapper evaluates its argument twice,
completed++ increments twice per loop iteration. This would skip completion
entries and eventually access out-of-bounds memory past the end of the cmds
array.
Could the argument be evaluated once and captured in a local variable
within these macros to avoid this?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=2
^ permalink raw reply
* Re: [PATCH v19 17/40] dept: apply timeout consideration to swait
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: cassel, Jeff Layton, Anna Schumaker, ojeda, dri-devel,
linux-rt-devel, tytso, dlemoal, linux-modules, linux-kernel,
linux-ide, Chuck Lever
In-Reply-To: <20260706061928.66713-18-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] When a wait event is interrupted by a signal, it bypasses sdt_might_sleep_end(), permanently leaking the DEPT wait stage.
--
commit 264d829a8f72641205caea8b130ee3c25557563a
Author: Byungchul Park <byungchul@sk.com>
dept: apply timeout consideration to swait
Now that CONFIG_DEPT_AGGRESSIVE_TIMEOUT_WAIT was introduced, apply the
consideration to swait, assuming an input 'ret' in ___swait_event()
macro is used as a timeout value.
> diff --git a/include/linux/swait.h b/include/linux/swait.h
> index 277ac74f61c39..233acdf55e9bc 100644
> --- a/include/linux/swait.h
> +++ b/include/linux/swait.h
> @@ -162,7 +162,7 @@ extern void finish_swait(struct swait_queue_head *q, struct swait_queue *wait);
> struct swait_queue __wait; \
> long __ret = ret; \
> \
> - sdt_might_sleep_start(NULL); \
> + sdt_might_sleep_start_timeout(NULL, __ret); \
[Severity: High]
Is this a regression where a signal-interrupted wait event permanently leaks
the dept wait stage tracking state?
Looking at ___swait_event(), if the task receives a signal while waiting, it
evaluates ___wait_is_interruptible(state) && __int as true and takes the
goto __out path:
if (___wait_is_interruptible(state) && __int) {
__ret = __int;
goto __out;
}
This jumps completely over the cleanup call:
finish_swait(&wq, &__wait);
sdt_might_sleep_end();
__out: __ret;
Without calling sdt_might_sleep_end() and its underlying dept_clean_stage(),
the dept state isn't cleaned up for the current task. Subsequent waits by this
task will fail to register new dependency maps and instead repeatedly commit
the stale wait map during __schedule(). This irreversibly corrupts the lock
dependency graph and triggers false deadlock reports.
The exact same bypass flaw appears to be present in ___wait_event() in
include/linux/wait.h and ___wait_var_event() in include/linux/wait_bit.h.
> INIT_LIST_HEAD(&__wait.task_list); \
> for (;;) { \
> long __int = prepare_to_swait_event(&wq, &__wait, state);\
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=17
^ permalink raw reply
* Re: [PATCH v19 01/40] dept: implement DEPT(DEPendency Tracker)
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: ojeda, tytso, Anna Schumaker, cassel, dlemoal, Jeff Layton,
dri-devel, linux-ide, linux-rt-devel, linux-kernel, Chuck Lever,
linux-modules
In-Reply-To: <20260706061928.66713-2-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Data race and Use-After-Clear on `dt->stage_m` between `dept_request_event_wait_commit()` and `dept_ttwu_stage_wait()`
- [High] Use-after-free and RCU list corruption because `dept_class` objects are freed before the RCU grace period elapses
- [High] Deadlock on PREEMPT_RT due to `local_irq_disable()` being used with a raw spinlock (`dept_pool_spin`)
--
commit 1fd2695b58a470d54e232dcaaf112ad8a807718e
Author: Byungchul Park <byungchul@sk.com>
dept: implement DEPT(DEPendency Tracker)
This commit implements DEPT (Dependency Tracker) to monitor waits and
events for deadlock detection. It addresses Lockdep's limitations
regarding read locks and cross-event scenarios.
> diff --git a/kernel/dependency/dept.c b/kernel/dependency/dept.c
> new file mode 100644
> index 0000000000000..52b9669fe0179
> --- /dev/null
> +++ b/kernel/dependency/dept.c
> @@ -0,0 +1,2977 @@
[ ... ]
> +static void dept_wq_work_fn(struct work_struct *work)
> +{
> + int i;
> +
> + for (i = 0; i < OBJECT_NR; i++) {
> + struct dept_pool *p = dept_pool + i;
> + int sz = p->tot_nr * p->obj_sz;
> + void *rpool;
> + bool need;
> +
> + local_irq_disable();
> + arch_spin_lock(&dept_pool_spin);
[Severity: High]
Is this locking pattern safe on PREEMPT_RT?
By using local_irq_disable() with a raw spinlock (dept_pool_spin), hardware
interrupts aren't disabled on PREEMPT_RT. Since DEPT instruments hardirq
paths via dept_enter(), could a hardware interrupt fire here, attempt to
acquire the exact same lock in from_pool(), and cause a hard deadlock?
Should raw_local_irq_disable() or arch_local_irq_save() be used instead?
[ ... ]
> + list_for_each_entry_safe(c, n, &dept_classes, all_node) {
> + if (!within((void *)c->key, start, sz) &&
> + !within(c->name, start, sz))
> + continue;
> +
> + hash_del_class(c);
> + disconnect_class(c);
> + list_del(&c->all_node);
> + invalidate_class(c);
> +
> + /*
> + * Actual deletion will happen on the rcu callback
> + * that has been added in disconnect_class().
> + */
> + del_class(c);
> + }
> + dept_unlock();
> + dept_exit(flags);
> +
> + /*
> + * Wait until even lockless hash_lookup_class() for the class
> + * returns NULL.
> + */
> + might_sleep();
> + synchronize_rcu();
[Severity: High]
Does this sequence safely handle RCU lifetimes for the dept_class objects?
Although the comment states actual deletion happens in the RCU callback,
disconnect_class() only queues callbacks for dept_dep objects, not the
class itself. By calling del_class(c) directly before synchronize_rcu(),
the object is pushed to the freelist synchronously via to_pool().
If a concurrent lockless RCU reader like hash_lookup_class() traverses
the bucket and reads hash_node.next after the class is reallocated,
wouldn't this result in list corruption or use-after-free crashes?
[ ... ]
> +void dept_request_event_wait_commit(void)
> +{
> + struct dept_task *dt = dept_task();
> + unsigned long flags;
> + unsigned int wg;
> + unsigned long ip;
> + const char *w_fn;
> + bool sched_map;
[ ... ]
> + wg = next_wgen();
> + WRITE_ONCE(dt->stage_m.wgen, wg);
> + arch_spin_unlock(&dt->stage_lock);
> +
> + __dept_wait(&dt->stage_m, 1UL, ip, w_fn, 0, true, sched_map);
[Severity: High]
Can this sequence lead to a use-after-clear of dt->stage_m?
Once dt->stage_lock is dropped, a concurrent wakeup can execute
dept_ttwu_stage_wait() which acquires the same stage_lock and calls
__dept_clean_stage(), clearing the original stage_m using memset().
Since __dept_wait() is called here with a reference to &dt->stage_m
after the lock is released, could it read memory that is actively being
zeroed out, resulting in a NULL pointer dereference for fields like
keys or name in check_new_class()?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=1
^ permalink raw reply
* Re: [PATCH v19 11/40] dept: apply sdt_might_sleep_{start,end}() to swait
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: linux-rt-devel, dlemoal, Chuck Lever, linux-kernel, linux-ide,
ojeda, tytso, dri-devel, cassel, linux-modules, Jeff Layton,
Anna Schumaker
In-Reply-To: <20260706061928.66713-12-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The `___swait_event`, `___wait_event`, and `___wait_var_event` macros skip the `sdt_might_sleep_end()` cleanup call when returning early due to a pending signal, leading to persistent DEPT state corruption that permanently disables dependency tracking for the affected task.
--
commit 7785a2fe54ced42e856f39ece5776f11bca47be8
Author: Byungchul Park <byungchul@sk.com>
dept: apply sdt_might_sleep_{start,end}() to swait
Make dept able to track dependencies by swaits.
> diff --git a/include/linux/swait.h b/include/linux/swait.h
> index d324419482a0f..277ac74f61c39 100644
> --- a/include/linux/swait.h
> +++ b/include/linux/swait.h
[ ... ]
> @@ -161,6 +162,7 @@ extern void finish_swait(struct swait_queue_head *q, struct swait_queue *wait);
> struct swait_queue __wait; \
> long __ret = ret; \
> \
> + sdt_might_sleep_start(NULL); \
> INIT_LIST_HEAD(&__wait.task_list); \
> for (;;) { \
> long __int = prepare_to_swait_event(&wq, &__wait, state);\
> @@ -176,6 +178,7 @@ extern void finish_swait(struct swait_queue_head *q, struct swait_queue *wait);
> cmd; \
> } \
> finish_swait(&wq, &__wait); \
> + sdt_might_sleep_end(); \
> __out: __ret; \
> })
[Severity: High]
Does this code bypass the sdt_might_sleep_end() cleanup when returning early
due to a pending signal?
Looking at ___swait_event(), if a task waiting interruptibly receives a
signal, prepare_to_swait_event() returns a non-zero error code.
The wait loop evaluates ___wait_is_interruptible(state) && __int as true and
executes a goto __out; which jumps completely over this new cleanup call.
If sdt_might_sleep_end() is skipped, does the task's dependency staging map
remain persistently populated, effectively causing subsequent calls to
dept_stage_wait() to silently exit early and disable DEPT tracking for that
task?
It looks like this identical sequence might also occur in the generic
waitqueue macros (___wait_event in include/linux/wait.h) and the hashed
waitqueue macros (___wait_var_event in include/linux/wait_bit.h).
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=11
^ permalink raw reply
* Re: [PATCH v19 05/40] dept: add proc knobs to show stats and dependency graph
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: linux-modules, cassel, linux-kernel, tytso, dri-devel,
Anna Schumaker, Jeff Layton, Chuck Lever, linux-rt-devel, ojeda,
linux-ide, dlemoal
In-Reply-To: <20260706061928.66713-6-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Lockless traversal of `dept_classes` and `fc->dep_head` in procfs read operations causes kernel panic due to LIST_POISON pointer dereference and Use-After-Free.
--
commit e2e753e7050a1aa0365bc001321fe4e79158b5b4
Author: Byungchul Park <byungchul@sk.com>
dept: add proc knobs to show stats and dependency graph
This commit introduces procfs knobs to expose internal stats and the dependency
graph for the dependency tracker at runtime.
> diff --git a/kernel/dependency/dept_proc.c b/kernel/dependency/dept_proc.c
> new file mode 100644
> index 0000000000000..97beaf397715f
> --- /dev/null
> +++ b/kernel/dependency/dept_proc.c
[ ... ]
> +static void *l_next(struct seq_file *m, void *v, loff_t *pos)
> +{
> + /*
> + * XXX: Serialize list traversal if needed. The following might
> + * give a wrong information on contention.
> + */
> + return seq_list_next(v, &dept_classes, pos);
> +}
> +
> +static void *l_start(struct seq_file *m, loff_t *pos)
> +{
> + /*
> + * XXX: Serialize list traversal if needed. The following might
> + * give a wrong information on contention.
> + */
> + return seq_list_start_head(&dept_classes, *pos);
> +}
[Severity: High]
Is it safe to traverse dept_classes without locks here?
If a concurrent module unload occurs, dept_free_range() calls list_del() on the
class node:
kernel/dependency/dept.c:dept_free_range() {
...
list_del(&c->all_node);
...
}
Because list_del() poisons the removed node's next pointer with LIST_POISON1,
a concurrent lockless reader in seq_list_next() will dereference LIST_POISON1
and cause a kernel panic, rather than just returning wrong information.
> +
> +static void l_stop(struct seq_file *m, void *v)
> +{
> +}
> +
> +static int l_show(struct seq_file *m, void *v)
> +{
> + struct dept_class *fc = list_entry(v, struct dept_class, all_node);
> + struct dept_dep *d;
> + const char *prefix;
> +
> + if (v == &dept_classes) {
> + seq_puts(m, "All classes:\n\n");
> + return 0;
> + }
> +
> + prefix = fc->sched_map ? "<sched> " : "";
> + seq_printf(m, "[%p] %s%s\n", (void *)fc->key, prefix, fc->name);
> +
> + /*
> + * XXX: Serialize list traversal if needed. The following might
> + * give a wrong information on contention.
> + */
> + list_for_each_entry(d, &fc->dep_head, dep_node) {
[Severity: High]
Does traversing fc->dep_head without rcu_read_lock() risk a use-after-free?
Since the dependency objects in this list are RCU-managed, traversing them
without holding rcu_read_lock() might allow the objects to be freed to the
pool while they are still being iterated.
> + struct dept_class *tc = d->wait->class;
> +
> + prefix = tc->sched_map ? "<sched> " : "";
> + seq_printf(m, " -> [%p] %s%s\n", (void *)tc->key, prefix, tc->name);
> + }
> + seq_puts(m, "\n");
> +
> + return 0;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=5
^ permalink raw reply
* Re: [PATCH v19 04/40] dept: tie to lockdep and IRQ tracing
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: linux-rt-devel, linux-kernel, dlemoal, cassel, tytso, Chuck Lever,
Anna Schumaker, linux-modules, Jeff Layton, ojeda, dri-devel,
linux-ide
In-Reply-To: <20260706061928.66713-5-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] dept_key_destroy() synchronously recycles dept_class objects to the lockless pool before the RCU grace period completes, causing UAF and list traversal corruption for concurrent lockless readers.
- [High] rwsem_acquire_read and rwsem_acquire incorrectly use ldt_lock(), breaking reader-writer dependency tracking and causing false-positive deadlocks.
- [High] ldt_init() is called before the `key` parameter is validated, causing DEPT to dereference a garbage pointer if `key` is NULL.
- [Medium] dept_key_init() and dept_key_destroy() are called on static keys before validation, erroneously disabling DEPT or corrupting tracking state.
- [Low] STATIC_LOCKDEP_MAP_INIT creates double-quoted strings for map names due to `#n` stringification in DEPT_MAP_INITIALIZER.
--
commit 6c1a95ee3182a2a82a1d61196d75acd387d7f3e3
Author: Byungchul Park <byungchul@sk.com>
dept: tie to lockdep and IRQ tracing
This patch integrates DEPT (Dependency Tracker) into the lockdep framework
and IRQ tracing paths. It modifies various locking primitives and lockdep
interfaces to initialize and track DEPT maps alongside lockdep maps.
> diff --git a/include/linux/lockdep.h b/include/linux/lockdep.h
> index 621566345406d..60b17a2ff555f 100644
> --- a/include/linux/lockdep.h
> +++ b/include/linux/lockdep.h
[ ... ]
> @@ -428,7 +431,8 @@ enum xhlock_context_t {
> * Note that _name must not be NULL.
> */
> #define STATIC_LOCKDEP_MAP_INIT(_name, _key) \
> - { .name = (_name), .key = (void *)(_key), }
> + { .name = (_name), .key = (void *)(_key), \
> + .dmap = DEPT_MAP_INITIALIZER(_name, _key) }
[Severity: Low]
Since _name is usually passed as a string literal to
STATIC_LOCKDEP_MAP_INIT, does passing it to DEPT_MAP_INITIALIZER, which
uses the stringification operator on its first argument, result in
double-quoted strings for map names in DEPT's debug output?
[ ... ]
> -#define rwsem_acquire(l, s, t, i) lock_acquire_exclusive(l, s, t, NULL, i)
> -#define rwsem_acquire_nest(l, s, t, n, i) lock_acquire_exclusive(l, s, t, n, i)
> -#define rwsem_acquire_read(l, s, t, i) lock_acquire_shared(l, s, t, NULL, i)
> -#define rwsem_release(l, i) lock_release(l, i)
[ ... ]
> +#define rwsem_acquire(l, s, t, i) \
> +do { \
> + ldt_lock(&(l)->dmap, s, t, NULL, i); \
> + lock_acquire_exclusive(l, s, t, NULL, i); \
> +} while (0)
> +#define rwsem_acquire_nest(l, s, t, n, i) \
> +do { \
> + ldt_lock(&(l)->dmap, s, t, n, i); \
> + lock_acquire_exclusive(l, s, t, n, i); \
> +} while (0)
> +#define rwsem_acquire_read(l, s, t, i) \
> +do { \
> + ldt_lock(&(l)->dmap, s, t, NULL, i); \
> + lock_acquire_shared(l, s, t, NULL, i); \
> +} while (0)
[Severity: High]
Is it intentional that rwsem_acquire_read() uses ldt_lock() instead of
ldt_rlock(), and rwsem_acquire() uses ldt_lock() instead of ldt_wlock()?
This appears to mistakenly model a shared lock as an exclusive lock. Does
this cause DEPT to incorrectly model concurrent read locks as mutually
exclusive, leading to false-positive circular dependency (deadlock) reports
when concurrent readers take an rwsem and interact with other locks?
> diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
> index 2d4c5bab5af88..dc97f2753ef81 100644
> --- a/kernel/locking/lockdep.c
> +++ b/kernel/locking/lockdep.c
> @@ -1224,6 +1224,8 @@ void lockdep_register_key(struct lock_class_key *key)
> struct hlist_head *hash_head;
> struct lock_class_key *k;
> unsigned long flags;
>
> + dept_key_init(&key->dkey);
> +
> if (WARN_ON_ONCE(static_obj(key)))
> return;
[Severity: Medium]
In lockdep_register_key(), dept_key_init() is called before the
static_obj() check. If a static key is improperly passed, lockdep gracefully
rejects it by warning and returning early.
However, processing a static key in dept_key_init() triggers a DEPT_STOP
abort. Should the DEPT initialization occur after validating the key?
[ ... ]
> @@ -4940,6 +4950,8 @@ void lockdep_init_map_type(struct lockdep_map *lock, const char *name,
> {
> int i;
>
> + ldt_init(&lock->dmap, &key->dkey, subclass, name);
> +
> for (i = 0; i < NR_LOCKDEP_CACHING_CLASSES; i++)
> lock->class_cache[i] = NULL;
[Severity: High]
In lockdep_init_map_type(), ldt_init() is called before the parameter
validation checks for a NULL key further down in the function.
If key is NULL, &key->dkey yields a non-NULL offset pointer. Does DEPT accept
this garbage pointer in valid_key(), causing a kernel panic when
check_new_class() dereferences it?
[ ... ]
> @@ -6588,6 +6608,8 @@ void lockdep_unregister_key(struct lock_class_key *key)
> bool found = false;
> bool need_callback = false;
>
> + dept_key_destroy(&key->dkey);
> +
> might_sleep();
>
> if (WARN_ON_ONCE(static_obj(key)))
> return;
[Severity: Medium]
Similarly to lockdep_register_key(), dept_key_destroy() is called here
unconditionally before the static_obj() check. If a driver incorrectly calls
dynamic lock destruction on a static key, it eagerly destroys its DEPT state
while lockdep retains it. Should this also be moved after the static_obj()
check?
[Severity: Critical]
Additionally, in dept_key_destroy() called here, the function removes a class
from the RCU hash table and drops its refcount to 0 via del_class(). This
recycles the object to the DEPT pool before synchronize_rcu() is called.
Can a concurrent lockless reader in hash_lookup_class() traversing the same
bucket via hlist_for_each_entry_rcu() access this recycled object, leading to
a use-after-free and hash list corruption?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=4
^ permalink raw reply
* Re: [PATCH v19 12/40] dept: apply sdt_might_sleep_{start, end}() to waitqueue wait
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: dri-devel, linux-rt-devel, cassel, Chuck Lever, linux-kernel,
dlemoal, linux-modules, linux-ide, ojeda, Jeff Layton, tytso,
Anna Schumaker
In-Reply-To: <20260706061928.66713-13-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The `sdt_might_sleep_end()` cleanup macro is bypassed when wait loops (`___wait_event`, `___swait_event`) are interrupted by a signal, permanently leaking a DEPT dependency stage.
--
commit 1300cac9c94eeb3955744b6d40ed8dce8cfc059f
Author: Byungchul Park <byungchul@sk.com>
dept: apply sdt_might_sleep_{start,end}() to waitqueue wait
Make dept able to track dependencies by waitqueue waits.
> diff --git a/include/linux/wait.h b/include/linux/wait.h
> index dce055e6add39..c8f8b44060fb9 100644
> --- a/include/linux/wait.h
> +++ b/include/linux/wait.h
[ ... ]
> @@ -305,6 +306,7 @@ extern void init_wait_entry(struct wait_queue_entry *wq_entry, int flags);
> struct wait_queue_entry __wq_entry; \
> long __ret = ret; /* explicit shadow */ \
> \
> + sdt_might_sleep_start(NULL); \
> init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
> for (;;) { \
> long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
> @@ -323,6 +325,7 @@ extern void init_wait_entry(struct wait_queue_entry *wq_entry, int flags);
> break; \
> } \
> finish_wait(&wq_head, &__wq_entry); \
> + sdt_might_sleep_end(); \
> __out: __ret; \
> })
[Severity: High]
Does this bypass sdt_might_sleep_end() when interrupted by a signal?
Looking at ___wait_event(), if ___wait_is_interruptible(state) and __int is
set, it will take the goto __out path:
if (___wait_is_interruptible(state) && __int) { \
__ret = __int; \
goto __out; \
} \
If that path is taken, sdt_might_sleep_end() is bypassed, which permanently
leaks a DEPT dependency stage. A stale wait context remains in the task's
dependency tracker state. When the task later sleeps for unrelated reasons,
the scheduler might attribute the sleep to the leaked context, leading to
false-positive lockup splats.
Also, does this same structural issue exist in ___swait_event() in
include/linux/swait.h for simple waitqueues?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=12
^ permalink raw reply
* Re: [PATCH v19 08/40] dept: add a mechanism to refill the internal memory pools on running out
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: Chuck Lever, tytso, dri-devel, linux-kernel, linux-modules,
linux-rt-devel, Jeff Layton, cassel, Anna Schumaker, dlemoal,
ojeda, linux-ide
In-Reply-To: <20260706061928.66713-9-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Per-CPU object pools (`lpool`) grow unboundedly because objects are never returned to the global pool, leading to endless `vmalloc()` allocations and potential OOM.
- [High] Un-ratelimited printk in the pool exhaustion path causes console flooding and soft lockups.
--
(Note: The format of this report is altered to use a free-form summary rather
than inline diff quotes due to recitation restrictions.)
commit 0f35ff40c325e46d2d4adab446907928b805d0e5
Author: Byungchul Park <byungchul@sk.com>
dept: add a mechanism to refill the internal memory pools on running out
This commit introduces a mechanism to dynamically refill internal memory
pools for the DEPT engine using an IRQ work and workqueue. It adds per-CPU
local pools and a vmalloc-based refill path when the static pools are
exhausted.
[Severity: High]
Could the per-CPU local pools (lpool) grow without bounds under certain
workloads?
Looking at to_pool() and from_pool() in kernel/dependency/dept.c, objects
are unconditionally added to the per-CPU lpool without any upper bound
check or reclaim logic. If a task allocates an object (like ecxt or wait)
on CPU A, migrates to CPU B, and frees it, the object goes into CPU B's
lpool.
Because objects are never returned to the global spool when the lpool grows
large, the global pool will eventually drain. When from_pool() sees an
empty global pool, it triggers request_rpool_refill(), which allocates new
memory via vmalloc() in dept_wq_work_fn().
Will continuous cross-CPU allocation and freeing indefinitely leak memory
into local pools and repeatedly trigger vmalloc allocations, potentially
leading to an out-of-memory condition?
[Severity: High]
Is it safe to use an un-ratelimited printk in the pool exhaustion path?
In kernel/dependency/dept.c:from_pool(), when a pool is exhausted, the
code calls DEPT_INFO() (which maps to pr_warn()) to notify the user. This
happens with hardware interrupts disabled via dept_enter().
Because the pool refill in dept_wq_work_fn() happens asynchronously in a
workqueue, the pool will remain empty until the workqueue runs. Every
subsequent lock acquisition during this window will synchronously print a
multi-line message to the console.
Could this flood the console and cause soft lockups or RCU stalls since
thousands of locks might be acquired by a single thread while waiting for
the async workqueue to execute?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=8
^ permalink raw reply
* Re: [PATCH v19 10/40] dept: apply sdt_might_sleep_{start, end}() to wait_for_completion()/complete()
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: Jeff Layton, linux-kernel, linux-ide, Anna Schumaker, tytso,
dlemoal, linux-modules, Chuck Lever, dri-devel, ojeda, cassel,
linux-rt-devel
In-Reply-To: <20260706061928.66713-11-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The `init_completion_map()` macro ignores the provided lockdep map argument `m` and sets `dmap` to `NULL`. This forces all generic wrappers like `flush_work()` to share a single, monolithic dependency class generated at the `wait_for_completion` macro call site.
--
commit c25e7af1e00aa68cd5900db21d080eadb8fccd71
Author: Byungchul Park <byungchul@sk.com>
dept: apply sdt_might_sleep_{start,end}() to wait_for_completion()/complete()
This commit enables the DEPT (Dependency Tracker) to track dependencies
introduced by wait_for_completion() and complete() calls. It adds a
dept_map to the completion structure and applies the appropriate sleep
start and end annotations.
> diff --git a/include/linux/completion.h b/include/linux/completion.h
> index fb29156765743..bd2c207481d68 100644
> --- a/include/linux/completion.h
> +++ b/include/linux/completion.h
[ ... ]
> @@ -26,14 +27,33 @@
> struct completion {
> unsigned int done;
> struct swait_queue_head wait;
> + struct dept_map dmap;
> };
>
> +#define init_completion(x) \
> +do { \
> + sdt_map_init(&(x)->dmap); \
> + __init_completion(x); \
> +} while (0)
> +
> +/*
> + * XXX: No use cases for now. Fill the body when needed.
> + */
> #define init_completion_map(x, m) init_completion(x)
[Severity: High]
Does this macro cause independent dependency graphs to merge? By ignoring the
lockdep map argument m and calling init_completion(x) instead, it seems
generic wrappers like flush_work() will be forced to share a single
monolithic dependency class generated at the wait site.
Could this lead DEPT to report false-positive deadlocks during normal
operations? For example, if Work A flushes Work B, and Work B flushes
Work C, both use the same flush_work() completion class. DEPT might see
Work B both waiting on and satisfying this global class, erroneously
detecting a self-cycle.
> -static inline void complete_acquire(struct completion *x) {}
> -static inline void complete_release(struct completion *x) {}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=10
^ permalink raw reply
* Re: [PATCH v19 13/40] dept: apply sdt_might_sleep_{start, end}() to hashed-waitqueue wait
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: Chuck Lever, tytso, Anna Schumaker, linux-kernel, dlemoal,
dri-devel, Jeff Layton, linux-rt-devel, ojeda, linux-modules,
cassel, linux-ide
In-Reply-To: <20260706061928.66713-14-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] The `sdt_might_sleep_end()` macro is bypassed when `___wait_var_event` is interrupted, leaving the DEPT tracker in an inconsistent state.
- [Medium] The `sdt_might_sleep_end()` macro is bypassed when `___wait_event` is interrupted, leaving the DEPT tracker in an inconsistent state.
--
commit 0bd2d42866fa6b852ee5317edcaf6890a90c3c65
Author: Byungchul Park <byungchul@sk.com>
dept: apply sdt_might_sleep_{start,end}() to hashed-waitqueue wait
Make dept able to track dependencies by hashed-waitqueue waits.
> diff --git a/include/linux/wait_bit.h b/include/linux/wait_bit.h
> index 9e29d79fc790a..179a616ad245a 100644
> --- a/include/linux/wait_bit.h
> +++ b/include/linux/wait_bit.h
[ ... ]
> @@ -257,6 +258,7 @@ extern wait_queue_head_t *__var_waitqueue(void *p);
> struct wait_bit_queue_entry __wbq_entry; \
> long __ret = ret; /* explicit shadow */ \
> \
> + sdt_might_sleep_start(NULL); \
> init_wait_var_entry(&__wbq_entry, var, \
> exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
> for (;;) { \
> @@ -274,6 +276,7 @@ extern wait_queue_head_t *__var_waitqueue(void *p);
> cmd; \
> } \
> finish_wait(__wq_head, &__wbq_entry.wq_entry); \
> + sdt_might_sleep_end(); \
> __out: __ret; \
> })
[Severity: Medium]
If this wait is interrupted by a signal, the loop executes a goto __out
which bypasses sdt_might_sleep_end(). Does this leave the dept tracker
in an inconsistent state where the task's stage remains permanently armed?
This could cause false positive dependency tracking splats on subsequent
waits, or fail to record new dependencies because the tracker is blinded by
the stale stage.
I noticed the exact same issue is present in ___wait_event():
include/linux/wait.h:___wait_event() {
...
if (___wait_is_interruptible(state) && __int) {
__ret = __int;
goto __out;
}
...
sdt_might_sleep_end();
__out: __ret;
})
[Severity: Medium]
Does ___wait_event() also skip sdt_might_sleep_end() when interrupted?
By missing the paired cleanup call, the task's stage could remain
permanently armed here as well.
Would it make sense to move the __out label above sdt_might_sleep_end(),
or otherwise ensure the state is cleaned up on the interrupted paths for
both of these macros?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=13
^ permalink raw reply
* Re: [PATCH v19 03/40] dept: add lock dependency tracker APIs
From: sashiko-bot @ 2026-07-07 7:33 UTC (permalink / raw)
To: Byungchul Park
Cc: dri-devel, linux-modules, cassel, linux-ide, Anna Schumaker,
tytso, linux-rt-devel, dlemoal, ojeda, Chuck Lever, Jeff Layton,
linux-kernel
In-Reply-To: <20260706061928.66713-4-byungchul@sk.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] `ldt_set_class` silently drops the lock name parameter, causing newly assigned lock classes to be registered in DEPT with the old lock name.
--
commit ca6be2708025c25889ef609a2203d1465096983d
Author: Byungchul Park <byungchul@sk.com>
dept: add lock dependency tracker APIs
Wrap the base APIs for easier annotation on typical lock.
> diff --git a/include/linux/dept_ldt.h b/include/linux/dept_ldt.h
> new file mode 100644
> index 0000000000000..8047d0a531f10
> --- /dev/null
> +++ b/include/linux/dept_ldt.h
[ ... ]
> +#define ldt_downgrade(m, i) \
> + do { \
> + if (dept_ecxt_holding(m, LDT_EVT_W)) \
> + dept_map_ecxt_modify(m, LDT_EVT_W, NULL, LDT_EVT_R, i, "downgrade", "read_unlock", -1);\
> + } while (0)
> +
> +#define ldt_set_class(m, n, k, sl, i) dept_map_ecxt_modify(m, LDT_EVT_ALL, k, 0UL, i, "lock_set_class", "(any)unlock", sl)
[Severity: Medium]
Does this code inadvertently drop the lock name parameter `n`?
When a caller invokes `lock_set_class` on a held lock, it passes the new
name `n`. Because `n` is completely omitted from the replacement list here,
`dept_map_ecxt_modify` won't receive the new name. This could cause newly
assigned lock classes to be registered in DEPT with the old lock name, which
might lead to incorrect lock names in dependency deadlock reports.
Inside `dept_map_ecxt_modify()`, `dept_map_reinit()` is called with `NULL`
for the name, leaving `m->name` unchanged. `check_new_class()` is then
called, which registers the new class key using the old `m->name`. Lockdep
subsequently updates `m->name` via `ldt_init`, but it is too late as the
DEPT class was already created with the old name.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706061928.66713-1-byungchul@sk.com?part=3
^ permalink raw reply
* Re: [PATCH v19 22/40] dept: track PG_locked with dept
From: Byungchul Park @ 2026-07-07 2:35 UTC (permalink / raw)
To: Matthew Wilcox
Cc: linux-kernel, max.byungchul.park, kernel_team, torvalds,
damien.lemoal, linux-ide, adilger.kernel, linux-ext4, mingo,
peterz, will, tglx, rostedt, joel, sashal, daniel.vetter,
duyuyang, johannes.berg, tj, tytso, david, amir73il, gregkh,
kernel-team, linux-mm, akpm, mhocko, minchan, hannes,
vdavydov.dev, sj, jglisse, dennis, cl, penberg, rientjes, vbabka,
ngupta, linux-block, josef, linux-fsdevel, jack, jlayton,
dan.j.williams, hch, djwong, dri-devel, rodrigosiqueiramelo,
melissa.srw, hamohammed.sa, harry.yoo, chris.p.wilson,
gwan-gyeong.mun, boqun.feng, longman, yunseong.kim, ysk,
yeoreum.yun, netdev, matthew.brost, her0gyugyu, corbet,
catalin.marinas, bp, x86, hpa, luto, sumit.semwal, gustavo,
christian.koenig, andi.shyti, arnd, lorenzo.stoakes, Liam.Howlett,
rppt, surenb, mcgrof, petr.pavlu, da.gomez, samitolvanen, paulmck,
frederic, neeraj.upadhyay, joelagnelf, josh, urezki,
mathieu.desnoyers, jiangshanlai, qiang.zhang, juri.lelli,
vincent.guittot, dietmar.eggemann, bsegall, mgorman, vschneid,
chuck.lever, neil, okorniev, Dai.Ngo, tom, trondmy, anna, kees,
bigeasy, clrkwllms, mark.rutland, ada.coupriediaz,
kristina.martsenko, wangkefeng.wang, broonie, kevin.brodsky, dwmw,
shakeel.butt, ast, ziy, yuzhao, baolin.wang, usamaarif642,
joel.granados, richard.weiyang, geert+renesas, tim.c.chen, linux,
alexander.shishkin, lillian, chenhuacai, francesco,
guoweikang.kernel, link, jpoimboe, masahiroy, brauner,
thomas.weissschuh, oleg, mjguzik, andrii, wangfushuai, linux-doc,
linux-arm-kernel, linux-media, linaro-mm-sig, linux-i2c,
linux-arch, linux-modules, rcu, linux-nfs, linux-rt-devel,
2407018371, dakr, miguel.ojeda.sandonis, neilb, bagasdotme,
wsa+renesas, dave.hansen, geert, ojeda, alex.gaynor, gary,
bjorn3_gh, lossin, a.hindborg, aliceryhl, tmgross, rust-for-linux
In-Reply-To: <akvueAxPl8aoLvMR@casper.infradead.org>
On Mon, Jul 06, 2026 at 07:05:44PM +0100, Matthew Wilcox wrote:
> On Mon, Jul 06, 2026 at 03:19:10PM +0900, Byungchul Park wrote:
> > Makes dept able to track PG_locked waits and events, which will be
> > useful in practice. See the following link that shows dept worked with
> > PG_locked and detected real issues in practice:
> >
> > https://lore.kernel.org/lkml/1674268856-31807-1-git-send-email-byungchul.park@lge.com/
>
> > @@ -219,6 +220,7 @@ struct page {
> > struct page *kmsan_shadow;
> > struct page *kmsan_origin;
> > #endif
> > + struct dept_ext_wgen pg_locked_wgen;
> > } _struct_page_alignment;
>
> I may not understand this quite correctly, but I think that tracking
> PG_locked dependencies in the struct page has both false positive and
> false negative problems.
>
> Imagine we have a file mapping M1 containing folio F1 at index 0 and F2
> at index 1. It is correct locking order to lock F1 before locking F2
> (for example when doing writeback). Later, M1 has its folios reclaimed
> and returned to the free pool. Then each is added to mapping M2, this
> time with folio F2 at index 8 and F1 at index 9. Now the correct order
> to lock these folios in the order F2 followed by F1.
First of all, I appreciate your feedback. Thanks!
That case doesn't generate any dependency unless any other waits are
involved in. That should be handled in xxx_nested manner e.g.
folio_lock_nested() that I need to introduce. The work is in progress.
> I don't see a part of this patch where we clear pg_locked_wgen when the
> page is returned to the page allocator. Maybe I missed that.
You are right. pg_locked_wgen doesn't get cleared. However, DEPT works
this way:
folio_lock()
wait_for_pg_locked_cleared()
set_pg_locked() // (1) update pg_locked_wgen to the current wgen
... // there might be other waits
folio_unlock()
clear_pg_locked() // (2) check if there have been any waits since (1)
In other words, it's guranteed that pg_locked_wgen has been updated e.i.
(1) when DEPT refers to pg_locked_wgen e.i. (2). So I don't think it's
a problem.
> I think we should be tracking PG_locked dependencies in the owner
> of the folio. For files, that would be in the struct address_space.
> For anon memory, I think that's in the anon_vma, but if somebody told
> me it was in some other structure, I wouldn't argue with them.
I think it's a good point but it's a classification issue. folios owned
by struct address_space should be classified to e.g. address_space_class
and ones owned by struct anon_vma should be classified to e.g.
anon_vma_class. I will work on it to apply the insight you just gave
but better do it as follow-up patches since the initial patchset is
already too big to get reviewed.
> This requires slightly more complexity than lockdep currently has.
> We don't want to use a lockdep class for each folio, obviously. So we
> need something to say "I already have folio F1 locked, is it OK to lock
From DEPT's perspective, folio_lock(F1) and folio_lock(F2) are waits and
folio_unlock(F1) and folio_unlock(F2) are events. Since DEPT tracks
dependencies with specified classes between waits and events, DEPT's
interest in the following example is to detect a situation like:
< context X >
folio_lock(address_space_class'ed F1)
...
folio_lock(anon_vma_class'ed F2)
...
folio_unlock(anon_vma_class'ed F2)
...
folio_unlock(address_space_class'ed F1)
< context Y >
folio_lock(anon_vma_class'ed any folio)
...
folio_lock(address_space_class'ed any folio)
...
folio_unlock(address_space_class'ed any folio)
...
folio_unlock(anon_vma_class'ed any folio)
However, the following pattern should be manually annotated by
developers like using folio_lock_nested() or something. DEPT cannot
work with it automatically:
folio_lock(address_space_class'ed F1)
...
folio_lock(address_space_class'ed F2)
...
folio_unlock(address_space_class'ed F2)
...
folio_unlock(address_space_class'ed F1)
or
folio_lock(anon_vma_class'ed F1)
...
folio_lock(anon_vma_class'ed F2)
...
folio_unlock(anon_vma_class'ed F2)
...
folio_unlock(anon_vma_class'ed F1)
These should be explicitly annotated by developers if it's intended:
folio_lock(address_space_class'ed F1)
...
folio_lock_nested(address_space_class'ed F2)
...
folio_unlock(address_space_class'ed F2)
...
folio_unlock(address_space_class'ed F1)
or
folio_lock(anon_vma_class'ed F1)
...
folio_lock_nested(anon_vma_class'ed F2)
...
folio_unlock(anon_vma_class'ed F2)
...
folio_unlock(anon_vma_class'ed F1)
> folio F2?". Essentially figuring out how we can track all folios in a
> given mapping the same way, and making sure that we don't deadlock on
> folios in the same mapping.
At the moment, as I told you, DEPT cannot work with dependencies between
the same class'ed folios. However, it'd be much better if DEPT can work
with even those cases. Could you provide a scenario where a deadlock
happens between the same class'ed ones? Any idea how to detect for the
cases?
> If F1 and F2 are in different mappings, it's not a deadlock if F1 is in a
> filesystem mapping and F2 is in its backing dev. It's also not a deadlock
> if F1 and F2 are both filesystem folios and the inodes are both locked.
> See vfs_lock_two_folios() in fs/remap_range.c.
Yeah.. DEPT is a tracker to track dependencies between waits and events
even across different contexts, but not a magic unfortunately. That
lock ordering issue - with the same class'ed ones - should be resolved
in the manual manner as vfs_lock_two_folios() does.
> I have much less knowledge about anonymous memory locking order.
> Maybe it doesn't happen. Or about locking one anon and one file folio.
> For slab memory, we don't sleep on PG_locked (it's used as a spinlock bit).
> For other kinds of memory ... I don't know. Page migration is fun.
Anyway, the sophisticated classification you mentioned is necessary for
DEPT to be better especially for folio locking mechanism.
Thanks again!
Byungchul
^ permalink raw reply
* Re: [PATCH v19 22/40] dept: track PG_locked with dept
From: Matthew Wilcox @ 2026-07-06 18:05 UTC (permalink / raw)
To: Byungchul Park
Cc: linux-kernel, max.byungchul.park, kernel_team, torvalds,
damien.lemoal, linux-ide, adilger.kernel, linux-ext4, mingo,
peterz, will, tglx, rostedt, joel, sashal, daniel.vetter,
duyuyang, johannes.berg, tj, tytso, david, amir73il, gregkh,
kernel-team, linux-mm, akpm, mhocko, minchan, hannes,
vdavydov.dev, sj, jglisse, dennis, cl, penberg, rientjes, vbabka,
ngupta, linux-block, josef, linux-fsdevel, jack, jlayton,
dan.j.williams, hch, djwong, dri-devel, rodrigosiqueiramelo,
melissa.srw, hamohammed.sa, harry.yoo, chris.p.wilson,
gwan-gyeong.mun, boqun.feng, longman, yunseong.kim, ysk,
yeoreum.yun, netdev, matthew.brost, her0gyugyu, corbet,
catalin.marinas, bp, x86, hpa, luto, sumit.semwal, gustavo,
christian.koenig, andi.shyti, arnd, lorenzo.stoakes, Liam.Howlett,
rppt, surenb, mcgrof, petr.pavlu, da.gomez, samitolvanen, paulmck,
frederic, neeraj.upadhyay, joelagnelf, josh, urezki,
mathieu.desnoyers, jiangshanlai, qiang.zhang, juri.lelli,
vincent.guittot, dietmar.eggemann, bsegall, mgorman, vschneid,
chuck.lever, neil, okorniev, Dai.Ngo, tom, trondmy, anna, kees,
bigeasy, clrkwllms, mark.rutland, ada.coupriediaz,
kristina.martsenko, wangkefeng.wang, broonie, kevin.brodsky, dwmw,
shakeel.butt, ast, ziy, yuzhao, baolin.wang, usamaarif642,
joel.granados, richard.weiyang, geert+renesas, tim.c.chen, linux,
alexander.shishkin, lillian, chenhuacai, francesco,
guoweikang.kernel, link, jpoimboe, masahiroy, brauner,
thomas.weissschuh, oleg, mjguzik, andrii, wangfushuai, linux-doc,
linux-arm-kernel, linux-media, linaro-mm-sig, linux-i2c,
linux-arch, linux-modules, rcu, linux-nfs, linux-rt-devel,
2407018371, dakr, miguel.ojeda.sandonis, neilb, bagasdotme,
wsa+renesas, dave.hansen, geert, ojeda, alex.gaynor, gary,
bjorn3_gh, lossin, a.hindborg, aliceryhl, tmgross, rust-for-linux
In-Reply-To: <20260706061928.66713-23-byungchul@sk.com>
On Mon, Jul 06, 2026 at 03:19:10PM +0900, Byungchul Park wrote:
> Makes dept able to track PG_locked waits and events, which will be
> useful in practice. See the following link that shows dept worked with
> PG_locked and detected real issues in practice:
>
> https://lore.kernel.org/lkml/1674268856-31807-1-git-send-email-byungchul.park@lge.com/
> @@ -219,6 +220,7 @@ struct page {
> struct page *kmsan_shadow;
> struct page *kmsan_origin;
> #endif
> + struct dept_ext_wgen pg_locked_wgen;
> } _struct_page_alignment;
I may not understand this quite correctly, but I think that tracking
PG_locked dependencies in the struct page has both false positive and
false negative problems.
Imagine we have a file mapping M1 containing folio F1 at index 0 and F2
at index 1. It is correct locking order to lock F1 before locking F2
(for example when doing writeback). Later, M1 has its folios reclaimed
and returned to the free pool. Then each is added to mapping M2, this
time with folio F2 at index 8 and F1 at index 9. Now the correct order
to lock these folios in the order F2 followed by F1.
I don't see a part of this patch where we clear pg_locked_wgen when the
page is returned to the page allocator. Maybe I missed that.
I think we should be tracking PG_locked dependencies in the owner
of the folio. For files, that would be in the struct address_space.
For anon memory, I think that's in the anon_vma, but if somebody told
me it was in some other structure, I wouldn't argue with them.
This requires slightly more complexity than lockdep currently has.
We don't want to use a lockdep class for each folio, obviously. So we
need something to say "I already have folio F1 locked, is it OK to lock
folio F2?". Essentially figuring out how we can track all folios in a
given mapping the same way, and making sure that we don't deadlock on
folios in the same mapping.
If F1 and F2 are in different mappings, it's not a deadlock if F1 is in a
filesystem mapping and F2 is in its backing dev. It's also not a deadlock
if F1 and F2 are both filesystem folios and the inodes are both locked.
See vfs_lock_two_folios() in fs/remap_range.c.
I have much less knowledge about anonymous memory locking order.
Maybe it doesn't happen. Or about locking one anon and one file folio.
For slab memory, we don't sleep on PG_locked (it's used as a spinlock bit).
For other kinds of memory ... I don't know. Page migration is fun.
^ permalink raw reply
* Re: [PATCH v6 4/4] kallsyms: add KUnit tests for lineinfo feature
From: sashiko-bot @ 2026-07-06 14:45 UTC (permalink / raw)
To: Sasha Levin; +Cc: linux-modules
In-Reply-To: <20260706142914.2398993-5-sashal@kernel.org>
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] Test failures on architectures using function descriptors
- [Medium] Silently failing tests due to missing Kconfig dependency for module lineinfo
- [Medium] Test flakiness due to incorrect assumption about compiler optimization and function size
- [Medium] Missing NULL pointer check for test buffer allocation leading to potential kernel panic
--
commit 799168a56f108698328a395b2e257d42cad0b5d4
Author: Sasha Levin <sashal@kernel.org>
kallsyms: add KUnit tests for lineinfo feature
Add a KUnit test module (CONFIG_LINEINFO_KUNIT_TEST) that verifies the
kallsyms lineinfo feature produces correct source file:line annotations
in stack traces.
> diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
> index 1244dcac2294a..257b5ca442f32 100644
> --- a/lib/Kconfig.debug
> +++ b/lib/Kconfig.debug
[ ... ]
> @@ -3092,6 +3092,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
[Severity: Medium]
If this test is built as a loadable module and CONFIG_KALLSYMS_LINEINFO_MODULES
is disabled, will the module lineinfo tests fail at runtime? Should this Kconfig
also depend on KALLSYMS_LINEINFO_MODULES when built as a module, or should the
test suite gracefully skip module-specific tests if that configuration is not
enabled?
> 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
[ ... ]
> +static char *alloc_sym_buf(struct kunit *test)
> +{
> + return kunit_kzalloc(test, KSYM_SYMBOL_LEN, GFP_KERNEL);
> +}
[Severity: Medium]
Can kunit_kzalloc() return NULL here and cause a kernel panic when callers
unconditionally pass the buffer to sprint_with_lineinfo()? Would it be better
to add a KUNIT_ASSERT_NOT_NULL() check here or validate the return value in
the callers?
[ ... ]
> +static noinline int lineinfo_target_many_lines(void)
> +{
> + int a = 0;
> +
> + barrier();
> + a += 1;
> + a += 2;
[ ... ]
> + a += 10;
> + barrier();
> + return a;
> +}
[Severity: Medium]
Is it possible that standard compiler optimizations will fold the arithmetic
in this function away, causing it to compile to fewer than 8 bytes?
If that happens, wouldn't the test_many_lines_mid_function() test below fail
because it adds 8 bytes to the start address and ends up looking outside the
function bounds? Should WRITE_ONCE() or inline assembly be used here to
prevent the arithmetic from being optimized out?
[ ... ]
> +static void test_normal_function(struct kunit *test)
> +{
> + char *buf = alloc_sym_buf(test);
> + unsigned long addr = (unsigned long)lineinfo_target_normal;
[Severity: Medium]
On architectures that use function descriptors (like PowerPC ELFv1 or ia64),
will casting lineinfo_target_normal directly to unsigned long yield a data
descriptor address instead of the instruction pointer?
Could this cause sprint_backtrace() to fail to resolve the symbol, breaking
the tests unconditionally on those architectures? Does this require using
dereference_symbol_descriptor() to get the actual instruction pointer?
[ ... ]
> +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);
[Severity: Medium]
Similarly, adding 1 to a function descriptor pointer instead of an instruction
pointer creates a corrupt address. Will this cause snprintf() with %pBb to
fail to format the backtrace correctly on architectures using function
descriptors?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706142914.2398993-1-sashal@kernel.org?part=4
^ permalink raw reply
* Re: [PATCH v6 1/4] kallsyms: embed source file:line info in kernel stack traces
From: sashiko-bot @ 2026-07-06 14:44 UTC (permalink / raw)
To: Sasha Levin; +Cc: linux-modules
In-Reply-To: <20260706142914.2398993-2-sashal@kernel.org>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Assembly routines and other un-annotated symbols will be falsely annotated with the file and line number of the preceding C function because the binary search does not restrict lookups to the boundaries of the resolved symbol.
- [Low] Unchecked `strdup` return value causes a segmentation fault on memory allocation failure in the `gen_lineinfo` host tool.
--
commit 8ad0e66e84394fea4bf24efbedda7eb50bbe8269
Author: Sasha Levin <sashal@kernel.org>
kallsyms: embed source file:line info in kernel stack traces
This commit adds CONFIG_KALLSYMS_LINEINFO to embed an address-to-line
lookup table in the kernel image. A new host tool extracts DWARF data
at build time, allowing stack traces to directly include source file and
line number information.
> diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
> index aec2f06858afd..1e3f527b13988 100644
> --- a/kernel/kallsyms.c
> +++ b/kernel/kallsyms.c
> @@ -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))
[Severity: Medium]
Will this falsely annotate assembly routines or un-annotated symbols with
the file and line number of the preceding C function?
The caller has access to the resolved symbol's size and offset but fails to
pass this bounding information to kallsyms_lookup_lineinfo(). The binary
search in kallsyms_lookup_lineinfo() does not restrict lookups to the
boundaries of the resolved symbol, finding the closest preceding entry.
Since gen_lineinfo explicitly ignores DW_LNE_end_sequence markers, could
this regression cause the search to bleed across symbol boundaries?
> + len += snprintf(buffer + len, KSYM_SYMBOL_LEN - len,
> + " (%s:%u)", li_file, li_line);
> + }
> +
> return len;
> }
> 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 @@
[ ... ]
> + 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);
[Severity: Low]
Does this code handle memory allocation failures?
strdup() can return NULL on failure, which would be assigned directly to the
files array here and could cause a segmentation fault when later dereferenced
via strlen().
> + 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;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706142914.2398993-1-sashal@kernel.org?part=1
^ permalink raw reply
* Re: [PATCH v6 2/4] kallsyms: extend lineinfo to loadable modules
From: sashiko-bot @ 2026-07-06 14:41 UTC (permalink / raw)
To: Sasha Levin; +Cc: linux-modules
In-Reply-To: <20260706142914.2398993-3-sashal@kernel.org>
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] `apply_debug_line_relocations()` ignores `.rel.debug_line` (SHT_REL), breaking module lineinfo on 32-bit architectures like i386 and ARM.
- [High] Misattribution of DWARF line entries for unhandled text sections pollutes `.text` lineinfo.
- [High] Hardcoded assembly padding creates an ABI mismatch with the C struct layout on architectures where `alignof(u64) == 4` (e.g., 32-bit x86).
- [Medium] An integer overflow in relocation offset validation allows an out-of-bounds write in the `gen_lineinfo` build tool.
--
commit a6a5ac22ccec63c8ea45f4ded1db6418cd100063
Author: Sasha Levin <sashal@kernel.org>
Subject: kallsyms: extend lineinfo to loadable modules
This commit extends the CONFIG_KALLSYMS_LINEINFO feature to loadable kernel
modules. It post-processes modules at build time to generate per-section
.mod_lineinfo binary tables mapping addresses to source lines. The tables
are embedded in the module and searched during stack traces.
> 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
[ ... ]
> + /* 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;
[Severity: High]
Does apply_debug_line_relocations() skip .rel.debug_line sections?
On 32-bit architectures like i386 and ARM, relocations use SHT_REL and
generate a .rel.debug_line section rather than .rela.debug_line.
If the function bails out here, synthetic biases are never added and
DW_FORM_line_strp offsets remain un-relocated, which feeds zeros to libdw.
Could this break the module lineinfo feature entirely on these architectures?
[ ... ]
> + if (module_mode) {
> + struct covered_section *cs;
> +
> + cs = section_by_index(all_sections, ALL_SECTIONS,
> + sym.st_shndx);
> + if (cs)
> + value += cs->bias;
> + }
[Severity: High]
Could unbiased text sections pollute the .text lineinfo?
Sections not listed in all_sections[], such as .noinstr.text, do not
receive a synthetic bias here, leaving their relocated DWARF PCs starting at 0.
When classify_address() processes these entries:
classify_address() {
...
if (addr < sections[i].bias)
continue;
if (addr >= sections[i].bias + sections[i].size)
continue;
...
}
An address smaller than the size of .text will fall into the .text bucket,
since .text has a bias of 0. Would this incorrectly merge line entries and
corrupt the binary search invariant?
[ ... ]
> + 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);
> + }
[Severity: Medium]
Is there a risk of an integer overflow in this bounds check?
If a maliciously crafted ELF file provides a huge r_offset value from the
relocation section (such as UINT64_MAX), the addition of width could wrap
around to a small positive integer and bypass the check. This might lead to
an out-of-bounds write in elf_write_uint().
[ ... ]
> +static void emit_blob(const char *output_section,
> + const char *blob_tag,
> + enum mod_lineinfo_blob blob)
> +{
[ ... ]
> + 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");
[Severity: High]
Does this hardcoded padding create an ABI mismatch with the mod_lineinfo_root
C struct on some architectures?
The assembly forces an 8-byte alignment immediately following the 4-byte
num_sections field. However, on 32-bit x86 architectures, the System V ABI
sets alignof(u64) to 4.
Looking at include/linux/mod_lineinfo.h:
struct mod_lineinfo_root {
u32 num_sections;
struct mod_lineinfo_section sections[];
};
The C compiler will align the struct to 4 bytes, meaning the sections[]
flexible array starts at offset 4, not offset 8. If the kernel reads this
blob, could it parse the assembly padding as the start of the anchor field
and corrupt the section descriptors?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706142914.2398993-1-sashal@kernel.org?part=2
^ permalink raw reply
* Re: [PATCH v6 3/4] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
From: sashiko-bot @ 2026-07-06 14:40 UTC (permalink / raw)
To: Sasha Levin; +Cc: linux-modules
In-Reply-To: <20260706142914.2398993-4-sashal@kernel.org>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] lineinfo_read_sleb128 contains a signed left shift that can overflow on malformed or large data, triggering a UBSAN splat in NMI/panic contexts.
--
commit 3756e632dbdf4d6db79f0e2c8a39f6103ac6e8ad
Author: Sasha Levin <sashal@kernel.org>
kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
This patch replaces the flat uncompressed parallel arrays for lineinfo with a
block-indexed, delta-encoded, ULEB128/SLEB128 varint compressed format to
significantly reduce size. Entries are grouped into blocks of 64 and decoded
entirely on the stack to ensure safety during NMI or panic contexts.
> 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
[ ... ]
> +/* Read an SLEB128 varint. Same safety guarantees as above. */
> +static inline int32_t lineinfo_read_sleb128(const u8 *data, u32 *pos, u32 end)
> +{
> + int32_t result = 0;
> + unsigned int shift = 0;
> + u8 byte = 0;
> +
> + while (*pos < end) {
> + byte = data[*pos];
> + (*pos)++;
> + result |= (int32_t)(byte & 0x7f) << shift;
[Severity: High]
Can this signed left shift trigger a UBSAN splat on malformed or large data?
For the 5th byte of an SLEB128 varint, shift will be 28. If the decoded byte
has a value greater than 7 (e.g. 0x7F), 127 << 28 evaluates to 3409130496,
which exceeds INT32_MAX.
Since left-shifting into or past the sign bit of a signed integer is
undefined behavior in C, a corrupted or maliciously crafted .mod_lineinfo
section could trigger a CONFIG_UBSAN_SHIFT splat.
If this function is called by lineinfo_search() during an NMI or panic
context, would the resulting UBSAN splat attempt to take locks and call
printk, potentially causing a deadlock or nested panic?
Perhaps the shift should be performed as unsigned:
result |= (int32_t)((u32)(byte & 0x7f) << shift);
> + shift += 7;
> + if (!(byte & 0x80))
> + break;
> + if (shift >= 32) {
> + while (*pos < end && (data[*pos] & 0x80))
> + (*pos)++;
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260706142914.2398993-1-sashal@kernel.org?part=3
^ permalink raw reply
* [PATCH v6 4/4] kallsyms: add KUnit tests for lineinfo feature
From: Sasha Levin @ 2026-07-06 14:29 UTC (permalink / raw)
To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
James Bottomley
Cc: Sasha Levin, 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
In-Reply-To: <20260706142914.2398993-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 1f58b4123a8ae7..6ac2786cdcbcbd 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 1244dcac2294ad..257b5ca442f327 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -3092,6 +3092,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 4ead57602eac46..877416f5f70a3e 100644
--- a/lib/tests/Makefile
+++ b/lib/tests/Makefile
@@ -37,6 +37,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 00000000000000..285d798cb6a3c4
--- /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 v6 3/4] kallsyms: delta-compress lineinfo tables for ~2.7x size reduction
From: Sasha Levin @ 2026-07-06 14:29 UTC (permalink / raw)
To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
James Bottomley
Cc: Sasha Levin, 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
In-Reply-To: <20260706142914.2398993-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 9cda3263a07847..a3c71434330203 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 d95387f51b4c08..1f58b4123a8ae7 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 d7374ce444d811..ffe4c658067ecc 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 819d6594c29371..2bec9f0e6afc55 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 e058c411371237..edd5b1092f0503 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 e1e08469b4f2fd..394690a23a2f7d 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 42662c4fbc6c94..94fbdad3df7c6e 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 v6 2/4] kallsyms: extend lineinfo to loadable modules
From: Sasha Levin @ 2026-07-06 14:29 UTC (permalink / raw)
To: Andrew Morton, Masahiro Yamada, Luis Chamberlain, Linus Torvalds,
Richard Weinberger, Juergen Gross, Geert Uytterhoeven,
James Bottomley
Cc: Sasha Levin, 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
In-Reply-To: <20260706142914.2398993-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 | 3 +
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, 1250 insertions(+), 91 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 c8ec124394354e..dd264830c8d5bf 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 b98d57b1ee1d5b..d7e1c1de7998f5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13935,6 +13935,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
diff --git a/include/linux/mod_lineinfo.h b/include/linux/mod_lineinfo.h
new file mode 100644
index 00000000000000..9cda3263a07847
--- /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 7566815fabbe8b..2bc0263b086d27 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 f004cf9a69d40a..79975a6e67c21b 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -2102,8 +2102,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 1e3f527b13988b..d95387f51b4c08 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 0fc11e45df9b91..819d6594c29371 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 46dd8d25a6058e..46bb2bf799d1ee 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 01a37ec872b905..2cdd76becb4d52 100644
--- a/scripts/Makefile.modfinal
+++ b/scripts/Makefile.modfinal
@@ -46,11 +46,17 @@ 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 $@
+
# Re-generate module BTFs if either module's .ko or vmlinux changed
%.ko: %.o %.mod.o .module-common.o $(objtree)/scripts/module.lds $(and $(CONFIG_DEBUG_INFO_BTF_MODULES),$(KBUILD_BUILTIN),$(objtree)/vmlinux) FORCE
+$(call if_changed,ld_ko_o)
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 00000000000000..832d290f3bf4cb
--- /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 699e760178f094..e1e08469b4f2fd 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
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