If you are writing your first Linux kernel module or device driver, you will sooner or later run into the term critical section in Linux kernel code. It sounds academic, but it describes a very real and very common bug class: two pieces of code touching the same piece of memory at the same time, with unpredictable results. This lecture is part of our free Linux kernel programming course and lays the foundation you need before you touch a single spinlock or mutex. We will keep the explanation practical, use only inline HTML diagrams (no confusing ASCII art), and build everything around the current 6.x kernel series so the knowledge stays relevant today.
Prerequisites
Before this lecture, you should be comfortable with:
- Writing and loading a basic Loadable Kernel Module (insmod / rmmod)
- Basic C pointers and global variables
- The idea that a Linux system has multiple CPU cores (SMP)
If you haven’t covered LKM basics yet, work through our Linux Kernel Module Programming series first, then come back here.
What Exactly Is a Critical Section in the Linux Kernel?
A critical section is simply a stretch of code that reads or updates data shared by more than one execution path — two threads, two CPUs, a process and an interrupt handler, or any combination of these. The rule that must always hold is the exclusivity rule: only one execution path may be inside that stretch of code at any given instant. Break that rule, and you get a data race — a bug where the outcome depends on timing rather than logic.
In user-space application programming this concept already exists (think of two browser tabs writing to the same file), but in kernel code the stakes are higher. A corrupted kernel data structure does not just crash one process — it can crash the entire machine, corrupt a filesystem, or in the worst case open a security hole.
Why Shared Kernel Data Is Dangerous
The Linux kernel is one large program shared by every process, driver, and interrupt on the system. A global counter, a linked list, a device’s status flags — anything declared outside a function, or allocated once and pointed to from multiple places, is a candidate for a race. The danger isn’t the sharing itself (sharing data is often necessary); the danger is unprotected concurrent access to that shared data.
| Kind of Data | Safe Without Locking? | Why |
|---|---|---|
| Local (stack) variables | Yes | Private to each function call/thread — never shared |
| True read-only constants | Yes | Nobody ever writes to it after init |
| Global counters / flags | No | Multiple CPUs can read-modify-write simultaneously |
| Linked lists / trees shared across drivers | No | Insertion/removal is a multi-step operation, not atomic |
| Per-CPU variables (used correctly) | Mostly yes | Each CPU has its own private copy |
const keyword. const only stops the compiler from letting you write through that particular pointer — it says nothing about whether some other part of the kernel might still modify the underlying memory through a different pointer.How SMP Systems Turn Shared Data Into a Data Race
Every phone, laptop, and server sold today has multiple CPU cores — this is called a Symmetric Multi-Processing (SMP) system, and the Linux kernel is built with CONFIG_SMP enabled by default on virtually every modern build. Picture a character device driver whose read() callback updates a global statistics struct. Two user-space processes, running on two different cores, can call read() on that same device file at literally the same nanosecond.
Result: two increments happened, but the counter only went up by one. One update was silently lost.
This “lost update” pattern is one of the most common data races in real driver code, and it is exactly the kind of bug that testing struggles to catch: it depends on delicate timing, so your driver may pass every test on your development board and still corrupt data occasionally on a customer’s device under heavy load.
A Realistic (Broken) Kernel Module Example
Below is an original, simplified example — not taken from any book — of a character device driver that increments a shared counter every time it is opened. It compiles and works fine under light use, and fails silently under real concurrent load.
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
/* Shared writable state — NOT protected by any lock */
static int open_count;
static int demo_open(struct inode *inode, struct file *file)
{
/* Two CPUs can execute the next line "at the same time" */
open_count = open_count + 1;
pr_info("demo: device opened %d times\n", open_count);
return 0;
}
static const struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = demo_open,
};
The line open_count = open_count + 1; is not a single CPU instruction; it is a read, an add, and a write. On an SMP system, two cores can each read the old value before either writes the new one back, and one increment is lost. This is the essence of a data race, and it is why the kernel provides atomic types, spinlocks, and mutexes — tools we cover in Part 2 of this lecture.
Real-World Use Cases Where This Matters
Packet counters updated both from the interrupt handler (packet arrival) and from user-space-triggered ioctl calls (reset stats) are a classic race site.
Producer-consumer buffers between a hardware interrupt and a worker thread need careful index management to avoid overwriting unread data.
A driver’s “current state” field, if updated from multiple code paths, can jump into an invalid state if writes interleave.
Common Mistakes and How to Catch Them
| Mistake | Symptom | How to Catch It |
|---|---|---|
| Assuming single-core behaviour on SMP hardware | Occasional wrong counters/statistics | Stress-test with multiple concurrent user-space clients |
Trusting the volatile keyword for synchronization | Corruption under real load despite “looking” protected | Code review — volatile is not a locking primitive |
| No automated race detection | Bugs discovered only in production | Enable KCSAN (Kernel Concurrency Sanitizer) in a test kernel build |
CONFIG_KCSAN=y in a debug build. Running your module under a KCSAN-enabled kernel is one of the fastest ways to catch races that manual testing would miss for months.Performance and Security Considerations
Performance: Identifying critical sections correctly matters for performance too — locking too broadly (protecting more code than necessary) creates unnecessary contention and slows every CPU down. The goal is always the smallest critical section that is still fully correct.
Security: Unprotected data races in kernel code are not just reliability bugs. Several publicly documented Linux kernel vulnerabilities over the years have been rooted in race conditions that allowed a local attacker to corrupt kernel memory or escalate privileges by winning a timing window. Treat every unprotected shared write in driver code as a potential security issue, not just a stability one.
Summary / Key Takeaways
- A critical section is any code that touches data shared across execution paths.
- The exclusivity rule says only one execution path may be inside it at a time.
- Modern SMP hardware makes concurrent execution the default, not the exception.
- Local variables and true read-only constants are safe; global mutable state is not.
constandvolatileare not substitutes for real locking.- KCSAN is the modern kernel tool for catching data races automatically.
Conclusion
Understanding what a critical section in the Linux kernel actually is — and learning to spot one in your own driver code — is the single most important skill before you write a single line of locking code. Get this mental model right, and spinlocks, mutexes, and atomic operations (which we cover in Part 2) will make immediate sense instead of feeling like arbitrary API calls. This concept is central to every module in our free Linux kernel development course and free Linux device drivers course, because nearly every real driver bug report eventually traces back to an unprotected critical section somewhere in the code.
FAQ: Critical Sections and Data Races in Linux Kernel
Q1. What is a critical section in the Linux kernel in simple terms?
It’s a piece of code that reads or writes data shared by more than one thread, CPU, or interrupt, and therefore must be executed by only one of them at a time.
Q2. What is the difference between a critical section and a data race?
A critical section is the code region itself. A data race is the bug that happens when that region is entered by more than one execution path at the same time without protection.
Q3. Do single-core (UP) embedded systems need to worry about data races?
Yes. Even on a single core, kernel preemption, blocking calls, and hardware interrupts can interleave execution inside a critical section — we cover this in Part 2.
Q4. Is the C volatile keyword enough to prevent a data race?
No. volatile only stops certain compiler optimizations; it does not provide mutual exclusion or memory ordering guarantees.
Q5. How do I detect data races in my own kernel module?
Build and boot a test kernel with CONFIG_KCSAN=y and exercise your driver under concurrent load; KCSAN will report racing accesses automatically.
Q6. Can a data race really cause a security vulnerability?
Yes. Unsynchronized access to shared kernel memory has historically been the root cause of real-world local privilege escalation bugs, which is why kernel security audits pay close attention to locking.
Q7. Where can I learn how to actually fix these races?
Part 2 of this lecture covers preemption, blocking I/O, interrupt context, and the locking primitives (spinlocks, mutexes) used to protect critical sections on modern kernels.
Master critical sections, locking, and Linux device driver concurrency step by step — 100% free at EmbeddedPathashala.
Explore Free Courses Read Part 2 →
2 Comments