Introduction
Welcome back! In our previous lesson, we dove into the powerful lockdep tool and learned how to diagnose deadlock scenarios by interpreting its reports. We saw how it can proactively detect both simple recursive deadlocks and more complex lock-inversion (AB-BA) deadlocks, even before they hang the system.
Now that we have the diagnosis, we need to find the cure. This lesson directly addresses the learning outcome: Resolve a diagnosed deadlock by applying a consistent lock ordering scheme to the kernel code. We will move from identifying the problem to implementing the solution, a skill absolutely essential for contributing to the Linux kernel and a common subject in senior-level embedded interviews.
By the end of this lesson, you will be able to analyze a lockdep report and apply standard strategies to refactor kernel code, eliminate the deadlock, and create a robust, correct patch.
1. The Core Principle: Consistent Lock Ordering
As we saw, the classic AB-BA deadlock occurs when two threads try to acquire the same two locks in opposite orders.

The fundamental solution to this problem is to enforce a strict global lock ordering. If all code paths in the entire kernel agree to acquire Lock A before acquiring Lock B, then an AB-BA deadlock between them becomes impossible. A thread might have to wait for another thread to release both locks, but it will never create a circular wait.
This rule is simple in theory but challenging in a codebase as vast and complex as the Linux kernel. A developer working in one subsystem may not be aware of the locking conventions in another. This is precisely why lockdep is so critical: it acts as the enforcer of these global ordering rules at runtime.
When lockdep flags an AB-BA deadlock, it has identified a violation of this principle. Our job as developers is to restore consistency. There are two primary strategies to achieve this:
- Re-order the lock acquisition: Change one of the code paths to follow the established order.
- Refactor the code to avoid nested locking: Restructure the code so that the nested lock acquisition is no longer needed. This can involve reducing the scope of a critical section or even eliminating a redundant lock.
Let's explore these strategies using real-world examples from the kernel.
2. Strategy 1: Refactoring to Break the Lock Chain
Sometimes, the best way to fix a lock inversion is to not hold both locks at the same time. This involves carefully analyzing why a lock is held and for how long. If you can release the first lock before acquiring the second, the dependency chain is broken.
A great example of this is a deadlock that was fixed in the kernel's NFC subsystem.
CVE-2025-71079: Linux Kernel NFC Race Condition Flaw
This document describes a real-world deadlock in the Linux kernel's NFC subsystem. Pay close attention to the analysis of the two conflicting code paths and the description of the patch that fixed it.
Read the 'Overview', 'Vulnerability Analysis', 'Root Cause', and 'Patch Information' sections. Focus on understanding the two locks involved (device_lock and rfkill_global_mutex) and the two execution paths that caused the AB-BA deadlock.
Analyzing the Fix
As you read, lockdep detected the following conflict:
-
Thread A (
rfkill_fop_write):- Acquires
rfkill_global_mutex. - Tries to acquire
device_lock.
- Acquires
-
Thread B (
nfc_unregister_device):- Acquires
device_lock. - Calls a function that tries to acquire
rfkill_global_mutex.
- Acquires
This is a textbook AB-BA deadlock. The solution described in the "Patch Information" section is to move the calls to rfkill_unregister() and rfkill_destroy() outside of the device_lock critical section.
Let's visualize the "before" and "after" of the problematic code path (nfc_unregister_device):
Before (Buggy):
// Pseudo-code for the buggy logic
mutex_lock(&dev->device_lock);
// ... other work ...
rfkill_unregister(dev->rfkill); // This function internally takes rfkill_global_mutex
// ...
mutex_unlock(&dev->device_lock);
Here, device_lock is held while the code attempts to acquire rfkill_global_mutex inside rfkill_unregister().
After (Fixed):
// Pseudo-code for the fix
struct rfkill *local_rfkill;
mutex_lock(&dev->device_lock);
// Grab the pointer we need
local_rfkill = dev->rfkill;
// Nullify the pointer inside the critical section to prevent other uses
dev->rfkill = NULL;
mutex_unlock(&dev->device_lock);
// Now, perform the operation outside the critical section
if (local_rfkill) {
rfkill_unregister(local_rfkill); // rfkill_global_mutex is taken here, but we no longer hold device_lock
}
By reducing the critical section to only protect the pointer itself (dev->rfkill), the developer broke the nested locking dependency. The code no longer holds device_lock when it needs to acquire rfkill_global_mutex, completely resolving the deadlock.
3. Strategy 2: Simplifying the Locking Model
Another powerful technique, often leading to cleaner code, is to question whether both locks are necessary in the first place. If the protection provided by one lock is a superset of the protection provided by another, you may be able to eliminate the redundant lock entirely.
Let's examine a real patch submitted to the spidev driver that does exactly this.
spi: spidev: fix lock inversion between spi_lock and buf_lock
This is a real kernel patch that fixed a reproducible deadlock in the widely-used spidev driver. It's a perfect example of how simplifying a locking scheme can resolve an inversion.
First, read the commit message at the top, which describes the AB-BA pattern between spi_lock and buf_lock. Then, read the 'Fix Approach' section, which explains the solution. Finally, skim the code diff at the end to see how the redundant buf_lock was removed from the code.
Analyzing the Fix
The lockdep report (summarized in the commit message) found this conflict:
-
Path A (
read/write):mutex_lock(&spidev->buf_lock)mutex_lock(&spidev->spi_lock)(insidespidev_sync)
-
Path B (
ioctl):mutex_lock(&spidev->spi_lock)mutex_lock(&spidev->buf_lock)
The developer could have simply reordered the locks in one path to match the other. However, a deeper analysis revealed a better solution. The original comment for buf_lock claimed it was for "triple duty," but all three of those duties were already effectively covered by spi_lock, which was needed anyway for the underlying SPI operations.
The buf_lock was redundant.
The solution was elegant:
- Remove
buf_lockfrom thespidev_datastructure entirely. - Modify all code paths (
read,write,ioctl) to use onlyspi_lockfor serialization.
By consolidating to a single lock, the possibility of a lock inversion between spi_lock and buf_lock was completely eliminated. This not only fixed the deadlock but also simplified the driver's logic, making it easier to understand and maintain.
4. Interview Scenario: Responding to Maintainer Feedback
This scenario simulates a common experience when contributing to an open-source project like the Linux kernel.
Scenario: You've developed a new feature for a storage driver. Your code introduces a new mutex, stats_lock, to protect a statistics counter. You submit your patch to the subsystem maintainer. A day later, the maintainer replies with a lockdep report showing an AB-BA deadlock.
The report shows:
- An existing code path for I/O operations locks
subsystem_wide_lockand then calls a function that locks your newstats_lock. - Your new code path for reading statistics locks
stats_lockand then lockssubsystem_wide_lockto ensure data consistency during the read.
Question: How would you respond to the maintainer? Describe two possible strategies for fixing this deadlock, and explain the trade-offs of each.
Click to see the analysis
Here is a professional and comprehensive way to approach this problem:
My Response Strategy:
First, I would thank the maintainer for catching the issue and providing the lockdep report. Then, I would propose two potential solutions for discussion:
Strategy 1: Enforce a Consistent Lock Order
- Implementation: I would modify my statistics-reading code path to acquire the locks in the same order as the existing I/O path. This means I would acquire
subsystem_wide_lockbefore acquiringstats_lock. - Trade-off (Pro): This is often the simplest and most direct fix. It respects the existing locking convention of the subsystem, making the change minimally invasive.
- Trade-off (Con): This might require holding the
subsystem_wide_lockfor a longer duration than my original code did. If this is a highly contended lock, my change could introduce a minor performance regression by reducing concurrency. I would mention this possibility to the maintainer and ask if it's an acceptable trade-off.
Strategy 2: Refactor to Avoid Nested Locking
- Implementation: I would investigate if I can avoid holding both locks simultaneously. Perhaps I can use atomic operations (like
atomic64_t) for the counters. If so,stats_lockcould be removed entirely, as atomic operations provide their own synchronization. Alternatively, I could try to read the statistics into local variables while holdingstats_lock, release it, and only then acquiresubsystem_wide_lockif I need other consistent data. - Trade-off (Pro): This approach could lead to a superior design with better performance and scalability by eliminating a lock or reducing lock contention.
- Trade-off (Con): This is potentially a more complex change. Using atomics might not be sufficient if the statistics are more complex than simple counters. Refactoring the logic might require a deeper understanding of the subsystem's invariants, which carries a higher risk of introducing a different bug if I'm not careful.
By presenting both options, I demonstrate a deep understanding of synchronization, show respect for the existing codebase, and collaborate with the maintainer to find the best possible solution.
Conclusion
In this lesson, we bridged the gap between diagnosing a deadlock and resolving it. You've seen that once lockdep identifies a lock ordering violation, you have a clear, actionable path to fixing it.
Key Takeaways:
- The fundamental rule for preventing AB-BA deadlocks is to enforce a strict, global lock acquisition order.
- When faced with a lock inversion, you have two primary strategies:
- Refactor the code to break the dependency chain, often by reducing the scope of a critical section so that you don't hold two locks at once.
- Simplify the locking model by identifying and removing redundant locks, which eliminates the possibility of inversion and often improves code clarity.
- Choosing the right strategy involves analyzing the purpose of each lock and considering the performance and maintenance trade-offs.
You have now completed the "Kernel Concurrency & Synchronization" module. You've journeyed from identifying race conditions to using a variety of primitives (spinlocks, mutexes, RCU, atomics) and finally to debugging and fixing complex deadlocks.
In our next lesson, we will begin a new major topic: Virtual & Physical Memory Management. We'll start by exploring the foundational concepts of virtual memory, page tables, and the crucial role of the Memory Management Unit (MMU) in modern operating systems.