This linux kernel lockdep tutorial is part of our free Linux kernel development course, and it teaches you how the kernel’s lockdep engine catches a specific, very common bug class: the self-deadlock. If you are building kernel modules and device drivers, understanding lockdep is one of the highest-leverage debugging skills you can pick up, because it turns a silent, hard-to-reproduce hang into a readable diagnostic report the moment the bug happens — often before your system even locks up.
We will build a small, original kernel module that deliberately contains a self-deadlock, enable lockdep on a modern kernel, read the warning it produces, and then fix the bug the right way. Everything here targets current long-term kernels (6.6, 6.12 LTS) rather than the older 5.x behaviour you’ll find in most existing tutorials.
What You Will Learn
- What lockdep is and why it exists as a runtime locking validator, not just a static checker
- How a spinlock self-deadlock actually happens inside kernel code
- How to enable and configure lockdep on a modern 6.12+ development kernel
- How to write a small original kernel module that reproduces a self-deadlock on purpose
- How to read a lockdep warning line by line, including the IRQ-state annotation
- How to fix the bug correctly, and which patterns prevent it in the first place
- Performance, security, and real-world implications of locking bugs in production drivers
Prerequisites
- Comfortable writing and inserting a basic loadable kernel module (LKM)
- Basic understanding of spinlocks and why they protect shared kernel data
- A disposable virtual machine for testing — never test deadlock-prone code on bare metal
- Kernel source tree or headers matching your test kernel (6.6 LTS or 6.12 LTS recommended)
What Is Lockdep, Really?
Lockdep is the kernel’s built-in runtime locking correctness validator. Unlike a static analyzer that only reads source code, lockdep watches every lock acquisition and release as your code actually executes, and it builds a live dependency graph of “which locks were held when this other lock was taken.” The moment a new acquisition pattern would allow a deadlock — even one that hasn’t happened yet on this particular run — lockdep prints a warning.
This matters enormously for driver and module authors because deadlocks are timing-dependent. A locking bug might run cleanly for weeks and then hang production hardware under load. Lockdep converts that probabilistic failure into a deterministic, first-occurrence warning during testing.
How a Self-Deadlock Actually Happens
A self-deadlock (also called a recursive-locking deadlock) happens when a single thread of execution tries to acquire a non-recursive lock that it is already holding. Spinlocks in the Linux kernel are non-recursive by design: a second acquisition attempt on the same CPU simply spins forever waiting for a release that will never come, because the only thread that could release it is stuck spinning.
This bug is deceptively easy to introduce once your code grows past a single function. A common trigger looks like this:
- Function A takes a spinlock to protect a shared structure.
- While still holding that lock, Function A calls a helper function B for convenience.
- Helper B, written independently or reused from elsewhere, takes the very same lock internally — because B was designed to be safe when called standalone.
- The CPU now spins forever inside B, waiting on a lock that A is still holding.
The bug is invisible by just reading either function in isolation. It only becomes obvious when you look at the call chain, which is exactly the kind of thing lockdep is built to catch automatically.
| Step | Code Executing | Lock State |
| 1 | stats_lock() is called in log_event() | stats_lock: HELD |
| 2 | log_event() calls sync_stats() while still holding the lock | stats_lock: HELD |
| 3 | sync_stats() internally calls stats_lock() again | stats_lock: SPINNING (waiting on self) |
| 4 | CPU never returns — deadlock | System hang |
Enabling Lockdep on a Kernel 6.12+ Test System
Lockdep is a debug feature and is not enabled by default in production kernels because of its runtime overhead. On a distro test kernel you usually need a debug kernel or your own build. The relevant kernel config options remain conceptually the same across 6.x releases:
CONFIG_DEBUG_KERNEL=y
CONFIG_LOCK_DEBUGGING_SUPPORT=y
CONFIG_PROVE_LOCKING=y
CONFIG_DEBUG_LOCKDEP=y
CONFIG_DEBUG_SPINLOCK=y
If you are using a stock distro kernel that already ships with these enabled (many debug-flavoured kernel packages do), you can check at runtime instead of rebuilding:
grep -i prove_locking /boot/config-$(uname -r)
zcat /proc/config.gz 2>/dev/null | grep PROVE_LOCKING
On kernel 6.12, lockdep’s core detection logic is unchanged in concept from earlier releases, but the reporting has been cleaned up and the tracepoint infrastructure around locking has matured, so warnings are easier to correlate with ftrace and BPF-based tooling if you want to go deeper.
Building an Original Kernel Module That Self-Deadlocks
To see lockdep in action, we’ll write a small, original module. It maintains an event counter behind a spinlock, and exposes a logging function. The bug: the logging function calls a “sync” helper while still holding the lock, and that helper re-acquires the same lock.
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/kernel.h>
static DEFINE_SPINLOCK(stats_lock);
static unsigned long event_count;
/* Bug: this helper assumes it is always called standalone */
static void sync_stats(void)
{
spin_lock(&stats_lock);
pr_info("epmod: stats synced, count=%lu\n", event_count);
spin_unlock(&stats_lock);
}
static void log_event(void)
{
spin_lock(&stats_lock);
event_count++;
/* Bug trigger: sync_stats() re-acquires stats_lock while we
* are still holding it here. */
sync_stats();
spin_unlock(&stats_lock);
}
static int __init epmod_init(void)
{
pr_info("epmod: loaded, triggering self-deadlock demo\n");
log_event();
return 0;
}
static void __exit epmod_exit(void)
{
pr_info("epmod: unloaded\n");
}
module_init(epmod_init);
module_exit(epmod_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala lockdep self-deadlock demo (original example)");
Build it with a standard out-of-tree Makefile and insert it inside your disposable VM:
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
sudo insmod epmod.ko
Important: if lockdep is not enabled, this module will genuinely hang your VM, because spin_lock() really does spin forever on the second acquisition. With lockdep enabled, it instead prints a warning and continues, because lockdep detects the unsafe pattern before the deadlock has to fully occur.
Reading the Lockdep Warning
With CONFIG_PROVE_LOCKING enabled, inserting the module above produces a warning in dmesg similar in structure to this (field names shown here are illustrative of the format, not copied from any external source):
WARNING: possible recursive locking detected
insmod/1842 is trying to acquire lock:
(stats_lock){+.+.}, at: sync_stats+0x1a/0x40 [epmod]
but task is already holding lock:
(stats_lock){+.+.}, at: log_event+0x22/0x50 [epmod]
other info that might help us debug this:
Possible unsafe locking scenario:
CPU0
----
lock(stats_lock);
lock(stats_lock);
*** DEADLOCK ***
May be due to missing lock nesting notation
Reading this top to bottom:
- “is trying to acquire lock” tells you the exact function and offset where the second, blocked acquisition happens — here, inside
sync_stats(). - “but task is already holding lock” shows you the first acquisition — here, inside
log_event()— and both lines reference the identical lock instance. - The four-character annotation such as
{+.+.}encodes the IRQ context in which the lock has previously been acquired and released. In short, a+means the lock has been taken with interrupts enabled, and a.means it has been taken with interrupts disabled outside interrupt context. This tells you whether the same bug could also manifest as an interrupt-context deadlock. - The “Possible unsafe locking scenario” block is lockdep restating the pattern in plain
lock(); lock();form so you don’t have to mentally trace two separate log lines.
| Symbol | Meaning |
| + | Lock has been acquired with hard IRQs enabled |
| . | Lock has been acquired with hard IRQs disabled, not from interrupt context |
| – | Lock has been acquired from softirq context |
Fixing the Self-Deadlock the Right Way
Once lockdep points at the exact pair of acquisitions, the fix is almost always structural rather than clever. The correct pattern here is to never call a lock-taking helper while already holding that same lock. There are two clean approaches:
| Approach | When to Use It |
| Split the helper into a locked wrapper and an unlocked “_locked” core function, and call the core function directly when the lock is already held. | Best general-purpose fix; keeps single responsibility clear. |
| Release the lock before calling the helper, then re-acquire it afterward if needed. | Fine when the protected state doesn’t need to stay consistent across the helper call. |
Applying the first approach to our example module:
static void sync_stats_locked(void)
{
/* caller already holds stats_lock */
pr_info("epmod: stats synced, count=%lu\n", event_count);
}
static void sync_stats(void)
{
spin_lock(&stats_lock);
sync_stats_locked();
spin_unlock(&stats_lock);
}
static void log_event(void)
{
spin_lock(&stats_lock);
event_count++;
sync_stats_locked(); /* no re-acquisition, no deadlock */
spin_unlock(&stats_lock);
}
Reinsert the fixed module and confirm dmesg is clean of lockdep warnings.
What Changed for Modern Kernels (6.12+ and Mainline PREEMPT_RT)
Older tutorials on this topic were written before PREEMPT_RT was merged into mainline (it landed in kernel 6.12). Two things are worth knowing if you’re testing on a current kernel:
- On a PREEMPT_RT-enabled kernel, spinlocks that are not explicitly marked raw are implemented as sleeping locks internally, but lockdep still treats the logical locking dependency the same way, so a self-deadlock is still caught the same way described above.
raw_spinlock_tremains a true, non-preemptible spinlock even under PREEMPT_RT, and self-deadlock detection applies to it identically.- Lockdep’s dependency-graph tracking has been incrementally hardened across 6.x releases to reduce false positives around lock classes initialized via
spin_lock_init()at different call sites, so warnings you see on 6.12 are generally more trustworthy than on older 5.x kernels.
Real-World Use Cases
Self-deadlocks are one of the most common first bugs introduced when a driver grows from a single-file prototype into a module with shared helper functions, interrupt handlers, and sysfs or procfs callbacks that all touch the same protected state. Network drivers, character device drivers with periodic housekeeping timers, and any code that mixes “public” API functions with “internal” helper functions are especially prone to this pattern once more than one engineer starts contributing to the same file.
Common Mistakes and Troubleshooting Tips
| Mistake | Why It Happens | Fix |
| Calling a “helper” function without checking its locking assumptions | Helper was originally written to be called standalone | Provide a “_locked” variant for callers that already hold the lock |
| Testing on bare metal instead of a VM | Deadlock hangs the whole machine, losing the dmesg buffer | Always test locking-heavy code in a disposable VM with a serial console or netconsole |
| Assuming lockdep is on by default | Production kernels disable it for performance | Verify CONFIG_PROVE_LOCKING before relying on it during testing |
Best Practices
- Keep lock scopes as small as possible; don’t call anything you haven’t personally audited while holding a lock.
- Name lock-holding helper functions clearly, for example with a “_locked” suffix, so the calling convention is obvious from the signature.
- Enable lockdep in every development and CI kernel build, never only in production troubleshooting.
- Treat every lockdep warning as a real bug, not noise — even “possible” deadlocks it reports are structurally real, even if a specific run didn’t hit them.
Performance Considerations
Lockdep adds measurable runtime overhead because it tracks every lock acquisition and release against a growing dependency graph. This is entirely acceptable for development and CI kernels but is not something you ship in production. Once your locking design is validated, build your release kernel with lockdep disabled and rely on it again only when investigating a new suspected locking issue.
Security Considerations
Unresolved deadlocks in kernel code are a denial-of-service risk: a locally triggerable self-deadlock in a driver’s ioctl or sysfs path can be used to hang a shared system deliberately. Treat lockdep warnings surfaced during fuzzing (for example with syzkaller) with the same priority as memory-safety bugs, since both classes can be triggered by unprivileged or low-privileged callers depending on the code path.
Summary / Key Takeaways
- A self-deadlock happens when a thread re-acquires a non-recursive lock it already holds, usually through an innocent-looking helper call.
- Lockdep detects this pattern proactively, using the held-lock and target-lock addresses plus IRQ-state annotations.
- The fix is structural: never call a lock-taking function while already holding that same lock; split into locked and unlocked variants instead.
- On kernel 6.12+ with mainline PREEMPT_RT, the same detection logic applies, with raw spinlocks retaining true non-preemptible semantics.
Conclusion
Lockdep turns one of the hardest classes of kernel bugs — timing-dependent deadlocks — into something you can catch reliably during ordinary module testing. If you’re following along with our free Linux kernel development course, make it a habit to test every new locking-heavy module with lockdep enabled before you consider it done. The five minutes it costs to enable CONFIG_PROVE_LOCKING is far cheaper than debugging a production hang with no diagnostic trail.
Frequently Asked Questions
1. What is lockdep in the Linux kernel?
Lockdep is the kernel’s runtime locking validator. It watches lock acquisitions and releases as code executes and reports unsafe locking patterns, including self-deadlocks, before they necessarily cause a full hang.
2. Does lockdep slow down my kernel?
Yes, noticeably. It’s a debug feature meant for development and CI kernels, not production builds.
3. Can lockdep catch bugs that haven’t happened yet?
Yes. Because it tracks the full dependency graph of lock ordering, it can flag a pattern as unsafe the first time it’s exercised, even if the actual deadlock would only occur under specific timing on a different run.
4. What’s the difference between a self-deadlock and an AB-BA deadlock?
A self-deadlock involves one thread re-acquiring one lock it already holds. An AB-BA deadlock involves two threads acquiring the same two locks in opposite order, each waiting on the other.
5. Is CONFIG_PROVE_LOCKING the same as CONFIG_DEBUG_SPINLOCK?
No. CONFIG_DEBUG_SPINLOCK adds basic sanity checks to spinlock operations. CONFIG_PROVE_LOCKING is the option that actually enables lockdep’s dependency-graph based deadlock detection.
6. Does mainline PREEMPT_RT change how lockdep works?
The underlying locking semantics change for non-raw spinlocks under PREEMPT_RT, but lockdep’s detection of unsafe locking patterns, including self-deadlocks, continues to apply.
7. Why did my system hang instead of printing a lockdep warning?
Most likely lockdep was not enabled on that kernel. Verify CONFIG_PROVE_LOCKING is set before relying on it, and always test on a disposable VM.
8. Can lockdep detect deadlocks involving mutexes, not just spinlocks?
Yes. Lockdep tracks spinlocks, mutexes, rwlocks, and other kernel lock primitives through the same dependency-graph mechanism.

1 Comment