| 0/0 |
2026/06/09 11:53 |
flow |
assessment-kcsan |
44m
Results: map[Benign:true Explanation:The KCSAN data race report identifies a race between `deactivate_file_folio()`
(the writer) and `__lru_add_drain_all()` (the reader) on the `nr` field of a
per-CPU `struct folio_batch`.
### Race Summary
* **Task A (CPU 0):** Executing `deactivate_file_folio()`, which calls
`folio_batch_add()`. This function performs a read-modify-write (`fbatch->nr++`)
to increment the number of folios in the per-CPU batch.
* **Task B (CPU 1):** Executing `__lru_add_drain_all()`, which calls
`cpu_needs_drain()`. This function performs a lockless read of `fbatch->nr` (via
`folio_batch_count()`) on remote CPUs to decide whether to schedule a drain work
item.
### Synchronization Context
The writer holds a `local_lock` (which on non-RT kernels is typically
`preempt_disable()`), ensuring mutual exclusion against other writers on the
same CPU. However, the reader on a remote CPU accesses the field without any
locks.
### Final Classification: **BENIGN**
### Reasoning
The race is structurally tolerant to stale reads and does not lead to logic
errors or crashes:
1. **No Tearing:** The racing variable `fbatch->nr` is an `unsigned char` (1
byte). On all supported architectures, a 1-byte access is atomic; the reader
will see either the old value or the new value, but never a corrupted/torn
value.
2. **Heuristic Optimization:** `cpu_needs_drain()` is a heuristic used to avoid
the overhead of queuing work items on CPUs that have empty batches. If the
reader sees a stale value of `0` when a folio has just been added, it simply
skips the drain for that CPU in the current round. This is a "missed
optimization" rather than a logic bug.
3. **Generation-Based Correctness:** The `__lru_add_drain_all()` function uses
a global generation counter (`lru_drain_gen`) and memory barriers (`smp_mb()`)
to ensure correctness. If a task requires its folios to be drained (e.g., for
`invalidate_bdev()`), the protocol guarantees that either this task or a
subsequent drainer will see the updated `nr` and perform the drain.
4. **Safe Draining:** When a drain is actually performed, it is done by a work
item running on the target CPU which acquires the `local_lock`. This ensures
that the drainer sees the fully initialized `folios[]` array and a consistent
`nr` count.
5. **Precedent:** Similar lockless checks of batch counts in `mm/swap.c` are
already acknowledged as safe and sometimes marked with `data_race()` (e.g., in
`lru_add_drain_cpu()`).
### Recommended Annotations
To silence the KCSAN report and prevent potential (though unlikely) compiler
optimizations from breaking the logic, the reads in `cpu_needs_drain()` should
be wrapped in `data_race()` or `READ_ONCE()`. Alternatively,
`folio_batch_count()` could be updated to use `READ_ONCE()`:
```c
static inline unsigned int folio_batch_count(const struct folio_batch *fbatch)
{
return READ_ONCE(fbatch->nr);
}
```]
|
| 1/1 |
2026/06/09 11:53 |
action |
kernel-checkouter |
0m
Results: map[KernelSrc:/app/workdir/cache/src/53697898c0882a319196c2d1f48549e9cf4f4a72]
|
| 2/1 |
2026/06/09 11:53 |
action |
kernel-builder |
22m
Results: map[KernelObj:/app/workdir/cache/build/ebdf0c7c807d63707e239aed39b9e0e405d65feb]
|
| 3/1 |
2026/06/09 12:15 |
action |
codesearch-prepare |
18m
Results: map[Index:codesearch-index]
|
| 4/1 |
2026/06/09 12:34 |
agent |
expert |
3m
Model:
Results:
Instruction: # KCSAN Data Race Severity Analysis Protocol
You are an expert Linux kernel concurrency engineer analyzing a Kernel
Concurrency Sanitizer (KCSAN) data race report to determine if it is
**BENIGN** or **HARMFUL**.
## 1. CLASSIFICATIONS
### **BENIGN (Truly Benign)**
The logic is sound and structurally tolerant to compiler optimizations or
stale/torn reads.
- **Diagnostics/Stats:** Reads used only for `/proc`, `/sys`, counters, or
`pr_info`.
- **Heuristic Hints:** A "hint" flag where an old value only causes a
slightly delayed update or a sub-optimal but safe fast-path.
- **Single-Writer Flag Updates:** A single writer updating flags where the
concurrent read is a simple bitwise check (e.g., `flags & MASK`). These are
historically tolerated, assuming neither "Fused Accesses" nor "Ordering
Violations" are relevant in this context.
- **Marked Reloads:** A load feeding into a `cmpxchg()` loop or checked
against a later `READ_ONCE()` reload.
- **Safe Overwrites:** Writing the same value already present.
### **HARMFUL (Logic Bug or Marking Required)**
The race causes incorrect behavior due to a synchronization failure or
because missing annotations allow the compiler to break the algorithm.
**Marking Required for Correctness:**
The algorithm is logically sound but requires annotations (`READ_ONCE()`,
`WRITE_ONCE()`, `smp_load_acquire()`, `smp_store_release()`, etc.) to be safe.
- **Fused Accesses:** The compiler might merge accesses or hoist a load out
of a loop, breaking polling/wait loops (livelocks).
- **Torn Accesses:** A large access (e.g., 64-bit on 32-bit arch) might be
split into multiple non-atomic accesses. Note that `READ_ONCE()` does **not**
guarantee atomicity for 64-bit variables on 32-bit architectures.
- **Ordering Violations:** The race breaks a "happens-before" relationship
(requires primitives with implied or explicit memory barriers).
**Logic Bugs:**
A fundamental synchronization failure. Marking accesses will **not** fix it;
the logic itself must change.
- **Pointers/Lifecycle:** The racing variable is a pointer being dereferenced
or a refcount governing object lifecycle (Use-After-Free risk).
- **Control Flow:** The variable guards a critical section, memory allocation,
or hardware command.
- **Bitfields:** Concurrent writes to different bits in the same word.
Compilers often use non-atomic read-modify-write sequences, meaning a
write to `bit_A` can "clobber" a concurrent write to `bit_B`. However,
do not blindly assume all bitfield accesses are harmful; you must prove
that a concurrent write actually clobbers another in a way that breaks
logic.
- **Complex Structures:** Races on shared lists, trees, or hashmaps.
- **Lossy Updates:** Concurrent plain RMW operations (e.g., `var++`) on
non-diagnostic variables where every increment must be preserved.
- **State Machines:** Races allowing a state machine to bypass transitions
or enter an invalid state.
- **Adjacent Unsynchronized Operations:** Consider races happening at the
same time. For example, if both threads execute `struct->has_elements = true;
list_add(node, &struct->list);`, the race on `has_elements` implies an
adjacent race on `list_head`, which is HARMFUL.
## 2. RESEARCH & ANALYSIS WORKFLOW
1. **Locate the Race:** Find the exact variables and functions in the stack
traces. Do not speculate about hypothetical compiler
behaviors or theoretical dangers (e.g., dismissing something as
"fundamentally unsafe") without tracing the actual data flow to a crash.
2. **Contextualize:** Identify held locks, RCU sections, or interrupt
contexts. Explain why the race is possible (e.g., "Thread A holds `lock`,
but Thread B is a lockless reader").
3. **Data Flow:** Follow the racing variable into its subsequent uses. If
the reader sees a stale or torn value, what branch is taken? Does it lead
to a pointer dereference or `BUG_ON()`? **You must prove harm via this
data flow.**
4. **Failure Sequencing (if HARMFUL):** Construct a two-column execution trace
demonstrating the failure.
*Example format:*
```text
CPU0 CPU1
function_a()
// lockless read
ptr = global_ptr
<PREEMPT>
function_b()
lock(&my_lock)
global_ptr = NULL
unlock(&my_lock)
kfree(ptr)
if (ptr)
*ptr = 1; // -> Use-After-Free!
```
## 3. OUTPUT FORMAT
- **Race Summary:** `[Function A]` vs `[Function B]` on `[Variable/Field]`.
- **Synchronization context:** List held locks and explain the lack of mutual
exclusion.
- **Final Classification:** **BENIGN** or **HARMFUL**.
**If BENIGN:**
- **Reasoning:** Briefly explain why the race is structurally tolerant to stale
or torn reads.
- **Recommended Annotations:** Suggest appropriate annotations (e.g.,
`READ_ONCE()`, `data_race()`).
**If HARMFUL:**
- **Failure Reasoning:** Explain and provide a two-column interleaving showing
exactly how the race leads to a failure such as corruption or kernel crash.
- **Recommended Fix (ONLY if fix is trivial):** Suggest a structural fix (e.g.,
"Hold `mapping->i_pages` lock", "Convert to `atomic_t`") or required memory
ordering annotations (e.g., "Wrap in `READ_ONCE()`", "Use
`smp_load_acquire()`").
Don't make assumptions about the kernel source code (it may be different from what you assume it is).
Extensively use the provided code access tools (codesearch-*, git-*, grepper, etc)
to examine the actual source code, and confirm any assumptions.
Prefer calling several tools at the same time to save round-trips.
Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.
Prompt:
The data race report is:
EXT4-fs (loop4): unmounting filesystem 00000000-0000-0000-0000-000000000000.
==================================================================
BUG: KCSAN: data-race in __lru_add_drain_all / deactivate_file_folio
read-write to 0xffff888237c267e8 of 1 bytes by task 3307 on cpu 0:
folio_batch_add include/linux/folio_batch.h:77 [inline]
__folio_batch_add_and_move mm/swap.c:194 [inline]
deactivate_file_folio+0xb6/0x1b0 mm/swap.c:701
mapping_try_invalidate+0x2c9/0x430 mm/truncate.c:567
invalidate_mapping_pages+0x27/0x40 mm/truncate.c:602
invalidate_bdev+0x58/0x70 block/bdev.c:102
ext4_put_super+0x610/0x7b0 fs/ext4/super.c:1355
generic_shutdown_super+0xee/0x220 fs/super.c:646
kill_block_super+0x2a/0x70 fs/super.c:1725
ext4_kill_sb+0x42/0x80 fs/ext4/super.c:7492
deactivate_locked_super+0x75/0x1c0 fs/super.c:476
deactivate_super+0x97/0xa0 fs/super.c:509
cleanup_mnt+0x2bb/0x330 fs/namespace.c:1312
__cleanup_mnt+0x19/0x20 fs/namespace.c:1319
task_work_run+0x130/0x1a0 kernel/task_work.c:233
resume_user_mode_work include/linux/resume_user_mode.h:50 [inline]
__exit_to_user_mode_loop kernel/entry/common.c:67 [inline]
exit_to_user_mode_loop+0x211/0x820 kernel/entry/common.c:98
__exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
do_syscall_64+0x232/0x3b0 arch/x86/entry/syscall_64.c:100
entry_SYSCALL_64_after_hwframe+0x77/0x7f
read to 0xffff888237c267e8 of 1 bytes by task 3306 on cpu 1:
folio_batch_count include/linux/folio_batch.h:56 [inline]
cpu_needs_drain mm/swap.c:788 [inline]
__lru_add_drain_all+0x1d5/0x450 mm/swap.c:883
lru_add_drain_all+0x10/0x20 mm/swap.c:899
invalidate_bdev+0x47/0x70 block/bdev.c:101
ext4_put_super+0x610/0x7b0 fs/ext4/super.c:1355
generic_shutdown_super+0xee/0x220 fs/super.c:646
kill_block_super+0x2a/0x70 fs/super.c:1725
ext4_kill_sb+0x42/0x80 fs/ext4/super.c:7492
deactivate_locked_super+0x75/0x1c0 fs/super.c:476
deactivate_super+0x97/0xa0 fs/super.c:509
cleanup_mnt+0x2bb/0x330 fs/namespace.c:1312
__cleanup_mnt+0x19/0x20 fs/namespace.c:1319
task_work_run+0x130/0x1a0 kernel/task_work.c:233
resume_user_mode_work include/linux/resume_user_mode.h:50 [inline]
__exit_to_user_mode_loop kernel/entry/common.c:67 [inline]
exit_to_user_mode_loop+0x211/0x820 kernel/entry/common.c:98
__exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
do_syscall_64+0x232/0x3b0 arch/x86/entry/syscall_64.c:100
entry_SYSCALL_64_after_hwframe+0x77/0x7f
value changed: 0x00 -> 0x01
Reported by Kernel Concurrency Sanitizer on:
CPU: 1 UID: 0 PID: 3306 Comm: syz-executor Not tainted syzkaller #0 PREEMPT(lazy)
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 05/09/2026
==================================================================
Reply: The KCSAN data race report identifies a race between `deactivate_file_folio()` (the writer) and `__lru_add_drain_all()` (the reader) on the `nr` field of a per-CPU `struct folio_batch`.
### Race Summary
* **Task A (CPU 0):** Executing `deactivate_file_folio()`, which calls `folio_batch_add()`. This function performs a read-modify-write (`fbatch->nr++`) to increment the number of folios in the per-CPU batch.
* **Task B (CPU 1):** Executing `__lru_add_drain_all()`, which calls `cpu_needs_drain()`. This function performs a lockless read of `fbatch->nr` (via `folio_batch_count()`) on remote CPUs to decide whether to schedule a drain work item.
### Synchronization Context
The writer holds a `local_lock` (which on non-RT kernels is typically `preempt_disable()`), ensuring mutual exclusion against other writers on the same CPU. However, the reader on a remote CPU accesses the field without any locks.
### Final Classification: **BENIGN**
### Reasoning
The race is structurally tolerant to stale reads and does not lead to logic errors or crashes:
1. **No Tearing:** The racing variable `fbatch->nr` is an `unsigned char` (1 byte). On all supported architectures, a 1-byte access is atomic; the reader will see either the old value or the new value, but never a corrupted/torn value.
2. **Heuristic Optimization:** `cpu_needs_drain()` is a heuristic used to avoid the overhead of queuing work items on CPUs that have empty batches. If the reader sees a stale value of `0` when a folio has just been added, it simply skips the drain for that CPU in the current round. This is a "missed optimization" rather than a logic bug.
3. **Generation-Based Correctness:** The `__lru_add_drain_all()` function uses a global generation counter (`lru_drain_gen`) and memory barriers (`smp_mb()`) to ensure correctness. If a task requires its folios to be drained (e.g., for `invalidate_bdev()`), the protocol guarantees that either this task or a subsequent drainer will see the updated `nr` and perform the drain.
4. **Safe Draining:** When a drain is actually performed, it is done by a work item running on the target CPU which acquires the `local_lock`. This ensures that the drainer sees the fully initialized `folios[]` array and a consistent `nr` count.
5. **Precedent:** Similar lockless checks of batch counts in `mm/swap.c` are already acknowledged as safe and sometimes marked with `data_race()` (e.g., in `lru_add_drain_cpu()`).
### Recommended Annotations
To silence the KCSAN report and prevent potential (though unlikely) compiler optimizations from breaking the logic, the reads in `cpu_needs_drain()` should be wrapped in `data_race()` or `READ_ONCE()`. Alternatively, `folio_batch_count()` could be updated to use `READ_ONCE()`:
```c
static inline unsigned int folio_batch_count(const struct folio_batch *fbatch)
{
return READ_ONCE(fbatch->nr);
}
```
|
| 5/2 |
2026/06/09 12:34 |
llm |
expert |
0m
Model:
Tokens:
input: 6462
output: 36
thoughts: 182
Thoughts: **Analyzing a Data Race in the Folio Batch System**
Okay, so I'm looking at this report and it looks like we've got a classic data race on our hands. The report highlights a conflict between `deactivate_file_folio` (which calls into `folio_batch_add`) and `__lru_add_drain_all` (which uses `folio_batch_count` and indirectly, `cpu_needs_drain`). The racing variable is at `0xffff888237c267e8`, and it's only one byte, which is interesting. It's probably some kind of counter or flag.
The report details that Task 3307 on CPU 0 is performing a read-write operation in `folio_batch_add`, which means an increment or update is likely happening there. At the same time, Task 3306 on CPU 1 is doing a read within `folio_batch_count`. Clearly, this is the crux of the problem.
My immediate next step is to examine the source code. I need to see what's happening within those functions. Let's start by looking at `include/linux/folio_batch.h` around lines 77 and 56 to get a better handle on the underlying data structures and potential locking mechanisms involved. I'll need to understand how these routines interact and where the race condition is occurring. It will also be important to see if there are any atomics in play here. This is classic. I'm excited to dive in.
|
| 6/2 |
2026/06/09 12:34 |
tool |
read-file |
0m
Args: map[File:include/linux/folio_batch.h FirstLine:40 LineCount:50]
Results: map[Contents: 40: */
41: static inline void folio_batch_init(struct folio_batch *fbatch)
42: {
43: fbatch->nr = 0;
44: fbatch->i = 0;
45: fbatch->percpu_pvec_drained = false;
46: }
47:
48: static inline void folio_batch_reinit(struct folio_batch *fbatch)
49: {
50: fbatch->nr = 0;
51: fbatch->i = 0;
52: }
53:
54: static inline unsigned int folio_batch_count(const struct folio_batch *fbatch)
55: {
56: return fbatch->nr;
57: }
58:
59: static inline unsigned int folio_batch_space(const struct folio_batch *fbatch)
60: {
61: return FOLIO_BATCH_SIZE - fbatch->nr;
62: }
63:
64: /**
65: * folio_batch_add() - Add a folio to a batch.
66: * @fbatch: The folio batch.
67: * @folio: The folio to add.
68: *
69: * The folio is added to the end of the batch.
70: * The batch must have previously been initialised using folio_batch_init().
71: *
72: * Return: The number of slots still available.
73: */
74: static inline unsigned folio_batch_add(struct folio_batch *fbatch,
75: struct folio *folio)
76: {
77: fbatch->folios[fbatch->nr++] = folio;
78: return folio_batch_space(fbatch);
79: }
80:
81: /**
82: * folio_batch_next - Return the next folio to process.
83: * @fbatch: The folio batch being processed.
84: *
85: * Use this function to implement a queue of folios.
86: *
87: * Return: The next folio in the queue, or NULL if the queue is empty.
88: */
89: static inline struct folio *folio_batch_next(struct folio_batch *fbatch)
]
|
| 7/2 |
2026/06/09 12:34 |
llm |
expert |
0m
Model:
Tokens:
input: 7673
output: 34
thoughts: 165
Thoughts: **Analyzing a Potential Race Condition**
Okay, so I'm digging into this code and I've got a potential race condition flagged. It's centered around this `fbatch->nr` variable. I see it's being read in `folio_batch_count`, but it's also being read-modified-written in `folio_batch_add`. Specifically, `fbatch->folios[fbatch->nr++] = folio;` is the key line there. That increment (`++`) is where the trouble *could* brew.
Now, let's look at who's actually using this. Task 3307, apparently, calls it indirectly through `deactivate_file_folio` and then `__folio_batch_add_and_move`. On the other hand, task 3306 uses `folio_batch_count` through `__lru_add_drain_all` and `cpu_needs_drain`. So we have one task writing the batch count, and another reading it at a different level.
My immediate concern is whether we have proper synchronization to protect the updates to `fbatch->nr`. If task 3307 and task 3306 are running concurrently without some sort of locking or atomic operation around `fbatch->nr`, we're likely to have a race. That could lead to incorrect batch counts and potentially corrupted data in `fbatch->folios`.
I'll now turn my attention to `mm/swap.c` because that area is prone to this type of contention. I need to understand the memory ordering and whether any locking primitives are in place. This will give me a better understanding of how significant the potential issue is.
|
| 8/2 |
2026/06/09 12:34 |
tool |
read-file |
0m
Args: map[File:mm/swap.c FirstLine:180 LineCount:100]
Results: map[Contents: 180: }
181:
182: static void __folio_batch_add_and_move(struct folio_batch __percpu *fbatch,
183: struct folio *folio, move_fn_t move_fn, bool disable_irq)
184: {
185: unsigned long flags;
186:
187: folio_get(folio);
188:
189: if (disable_irq)
190: local_lock_irqsave(&cpu_fbatches.lock_irq, flags);
191: else
192: local_lock(&cpu_fbatches.lock);
193:
194: if (!folio_batch_add(this_cpu_ptr(fbatch), folio) ||
195: !folio_may_be_lru_cached(folio) || lru_cache_disabled())
196: folio_batch_move_lru(this_cpu_ptr(fbatch), move_fn);
197:
198: if (disable_irq)
199: local_unlock_irqrestore(&cpu_fbatches.lock_irq, flags);
200: else
201: local_unlock(&cpu_fbatches.lock);
202: }
203:
204: #define folio_batch_add_and_move(folio, op) \
205: __folio_batch_add_and_move( \
206: &cpu_fbatches.op, \
207: folio, \
208: op, \
209: offsetof(struct cpu_fbatches, op) >= \
210: offsetof(struct cpu_fbatches, lock_irq) \
211: )
212:
213: static void lru_move_tail(struct lruvec *lruvec, struct folio *folio)
214: {
215: if (folio_test_unevictable(folio))
216: return;
217:
218: lruvec_del_folio(lruvec, folio);
219: folio_clear_active(folio);
220: lruvec_add_folio_tail(lruvec, folio);
221: __count_vm_events(PGROTATED, folio_nr_pages(folio));
222: }
223:
224: /*
225: * Writeback is about to end against a folio which has been marked for
226: * immediate reclaim. If it still appears to be reclaimable, move it
227: * to the tail of the inactive list.
228: *
229: * folio_rotate_reclaimable() must disable IRQs, to prevent nasty races.
230: */
231: void folio_rotate_reclaimable(struct folio *folio)
232: {
233: if (folio_test_locked(folio) || folio_test_dirty(folio) ||
234: folio_test_unevictable(folio) || !folio_test_lru(folio))
235: return;
236:
237: folio_batch_add_and_move(folio, lru_move_tail);
238: }
239:
240: void lru_note_cost_unlock_irq(struct lruvec *lruvec, bool file,
241: unsigned int nr_io, unsigned int nr_rotated)
242: __releases(lruvec->lru_lock)
243: __releases(rcu)
244: {
245: unsigned long cost;
246:
247: /*
248: * Reflect the relative cost of incurring IO and spending CPU
249: * time on rotations. This doesn't attempt to make a precise
250: * comparison, it just says: if reloads are about comparable
251: * between the LRU lists, or rotations are overwhelmingly
252: * different between them, adjust scan balance for CPU work.
253: */
254: cost = nr_io * SWAP_CLUSTER_MAX + nr_rotated;
255: if (!cost) {
256: spin_unlock_irq(&lruvec->lru_lock);
257: rcu_read_unlock();
258: return;
259: }
260:
261: for (;;) {
262: unsigned long lrusize;
263:
264: /* Record cost event */
265: if (file)
266: lruvec->file_cost += cost;
267: else
268: lruvec->anon_cost += cost;
269:
270: /*
271: * Decay previous events
272: *
273: * Because workloads change over time (and to avoid
274: * overflow) we keep these statistics as a floating
275: * average, which ends up weighing recent refaults
276: * more than old ones.
277: */
278: lrusize = lruvec_page_state(lruvec, NR_INACTIVE_ANON) +
279: lruvec_page_state(lruvec, NR_ACTIVE_ANON) +
]
|
| 9/2 |
2026/06/09 12:34 |
llm |
expert |
0m
Model:
Tokens:
input: 9761
output: 34
thoughts: 25
Thoughts: Okay, here's my interpretation of those thoughts, tailored for an expert audience, presented in the first person:
**Prioritizing Resource Management: `cpu_needs_drain` and `__lru_add_drain_all`**
Right, I'm digging into this code and I need to zoom in on a specific area related to resource management. The flow looks like it's becoming a bottleneck and I need to understand it better. I immediately need to zero in on two key identifiers. First, `cpu_needs_drain`. That likely signifies a condition where the CPU, for whatever reason, requires some kind of draining or flushing of its internal buffers or processing queue. This hints at either memory pressure, a potential synchronization issue, or maybe some form of data invalidation. Understanding *why* that flag is set is critical.
And that ties directly into the second function I want to examine, `__lru_add_drain_all`. The use of "LRU" strongly implies a Least Recently Used cache or list management strategy. The prefix "__" often denotes an internal or helper function. The combination of both indicates that there's some kind of cache management going on, likely in connection with that CPU draining need. So, the question that comes to mind is what is the context behind this particular cache? What kind of data is being cached here? Is the drain action related to the cache eviction process, some form of data synchronization, or garbage collection? Is there a relation with resource starvation?
My focus shifts now to the interplay between the condition signaled by `cpu_needs_drain` and the action performed by `__lru_add_drain_all`. I need to understand the connection. Is the drain action triggered by an LRU eviction, or is the LRU eviction a *response* to the CPU drain signal? Or, are they entirely unrelated but operating at similar stages in the program? This is the core of the problem, and I'll see what the context reveals.
|
| 10/2 |
2026/06/09 12:34 |
tool |
read-file |
0m
Args: map[File:mm/swap.c FirstLine:780 LineCount:120]
Results: map[Contents: 780:
781: static bool cpu_needs_drain(unsigned int cpu)
782: {
783: struct cpu_fbatches *fbatches = &per_cpu(cpu_fbatches, cpu);
784:
785: /* Check these in order of likelihood that they're not zero */
786: return folio_batch_count(&fbatches->lru_add) ||
787: folio_batch_count(&fbatches->lru_move_tail) ||
788: folio_batch_count(&fbatches->lru_deactivate_file) ||
789: folio_batch_count(&fbatches->lru_deactivate) ||
790: folio_batch_count(&fbatches->lru_lazyfree) ||
791: folio_batch_count(&fbatches->lru_activate) ||
792: need_mlock_drain(cpu) ||
793: has_bh_in_lru(cpu, NULL);
794: }
795:
796: /*
797: * Doesn't need any cpu hotplug locking because we do rely on per-cpu
798: * kworkers being shut down before our page_alloc_cpu_dead callback is
799: * executed on the offlined cpu.
800: * Calling this function with cpu hotplug locks held can actually lead
801: * to obscure indirect dependencies via WQ context.
802: */
803: static inline void __lru_add_drain_all(bool force_all_cpus)
804: {
805: /*
806: * lru_drain_gen - Global pages generation number
807: *
808: * (A) Definition: global lru_drain_gen = x implies that all generations
809: * 0 < n <= x are already *scheduled* for draining.
810: *
811: * This is an optimization for the highly-contended use case where a
812: * user space workload keeps constantly generating a flow of pages for
813: * each CPU.
814: */
815: static unsigned int lru_drain_gen;
816: static struct cpumask has_work;
817: static DEFINE_MUTEX(lock);
818: unsigned cpu, this_gen;
819:
820: /*
821: * Make sure nobody triggers this path before mm_percpu_wq is fully
822: * initialized.
823: */
824: if (WARN_ON(!mm_percpu_wq))
825: return;
826:
827: /*
828: * Guarantee folio_batch counter stores visible by this CPU
829: * are visible to other CPUs before loading the current drain
830: * generation.
831: */
832: smp_mb();
833:
834: /*
835: * (B) Locally cache global LRU draining generation number
836: *
837: * The read barrier ensures that the counter is loaded before the mutex
838: * is taken. It pairs with smp_mb() inside the mutex critical section
839: * at (D).
840: */
841: this_gen = smp_load_acquire(&lru_drain_gen);
842:
843: /* It helps everyone if we do our own local drain immediately. */
844: lru_add_drain();
845:
846: mutex_lock(&lock);
847:
848: /*
849: * (C) Exit the draining operation if a newer generation, from another
850: * lru_add_drain_all(), was already scheduled for draining. Check (A).
851: */
852: if (unlikely(this_gen != lru_drain_gen && !force_all_cpus))
853: goto done;
854:
855: /*
856: * (D) Increment global generation number
857: *
858: * Pairs with smp_load_acquire() at (B), outside of the critical
859: * section. Use a full memory barrier to guarantee that the
860: * new global drain generation number is stored before loading
861: * folio_batch counters.
862: *
863: * This pairing must be done here, before the for_each_online_cpu loop
864: * below which drains the page vectors.
865: *
866: * Let x, y, and z represent some system CPU numbers, where x < y < z.
867: * Assume CPU #z is in the middle of the for_each_online_cpu loop
868: * below and has already reached CPU #y's per-cpu data. CPU #x comes
869: * along, adds some pages to its per-cpu vectors, then calls
870: * lru_add_drain_all().
871: *
872: * If the paired barrier is done at any later step, e.g. after the
873: * loop, CPU #x will just exit at (C) and miss flushing out all of its
874: * added pages.
875: */
876: WRITE_ONCE(lru_drain_gen, lru_drain_gen + 1);
877: smp_mb();
878:
879: cpumask_clear(&has_work);
]
|
| 11/2 |
2026/06/09 12:34 |
llm |
expert |
0m
Model:
Tokens:
input: 11802
output: 33
thoughts: 18
Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience, and delivered as if they were my own:
**Navigating the `__lru_add_drain_all` Code Path**
Alright, I'm at the point where I've hit a conceptual roadblock. I recognize the call to `__lru_add_drain_all` and now I need a full understanding of its function within the current system. I know this function is going to be integral to understanding the inner workings of this LRU cache. It's almost guaranteed to be involved in either the removal or relocation of cached entries.
I need the rest of the implementation. Right now, I can only make assumptions based on the naming and context. I'm thinking about how the draining action is performed β is it synchronous or asynchronous? How does this function interact with other components of the cache like eviction policies or concurrency control, if any? Also, I need to understand what's *drained* and what are the triggers. Is it based on a timer, memory pressure, or some other trigger? A full understanding of `__lru_add_drain_all` is essential. Until I see its full implementation, I can't thoroughly comprehend how this LRU implementation really works.
|
| 12/2 |
2026/06/09 12:34 |
tool |
read-file |
0m
Args: map[File:mm/swap.c FirstLine:880 LineCount:20]
Results: map[Contents: 880: for_each_online_cpu(cpu) {
881: struct work_struct *work = &per_cpu(lru_add_drain_work, cpu);
882:
883: if (cpu_needs_drain(cpu)) {
884: INIT_WORK(work, lru_add_drain_per_cpu);
885: queue_work_on(cpu, mm_percpu_wq, work);
886: __cpumask_set_cpu(cpu, &has_work);
887: }
888: }
889:
890: for_each_cpu(cpu, &has_work)
891: flush_work(&per_cpu(lru_add_drain_work, cpu));
892:
893: done:
894: mutex_unlock(&lock);
895: }
896:
897: void lru_add_drain_all(void)
898: {
899: __lru_add_drain_all(false);
]
|
| 13/2 |
2026/06/09 12:34 |
llm |
expert |
0m
Model:
Tokens:
input: 12438
output: 35
thoughts: 962
Thoughts: **Analyzing a Potential Race Condition in Folio Batch Draining**
Okay, so I'm looking at a potential race condition in the folio batch draining logic, specifically between `cpu_needs_drain(cpu)` and `folio_batch_add(this_cpu_ptr(fbatch), folio)`. Essentially, the question is whether `cpu_needs_drain` can reliably read the updated count of folios in a batch, or if it might miss an increment due to a memory ordering issue.
The core of the problem lies in `__folio_batch_add_and_move`. This function is where folios are added to a per-CPU batch (`fbatch`). The critical sections are protected by either `local_lock_irqsave` or `local_lock`, depending on whether interrupts are disabled. Critically, `local_lock` only provides protection *within* the same CPU. This means it doesn't guarantee visibility across different CPUs. The lock only prevents races among tasks on the same CPU, which are handled appropriately.
`cpu_needs_drain(cpu)` is called from `__lru_add_drain_all`, which iterates through all online CPUs. It reads `fbatch->nr`, which represents the number of folios in the batch. The critical bit is that it does this *without* acquiring any locks. It uses the `folio_batch_count` function, which gets `fbatches->lru_deactivate_file.nr`.
The race: `folio_batch_add` increments `fbatch->nr` on CPU X, while `cpu_needs_drain` reads the value on CPU Y. If CPU Y reads an outdated value, the drain might be skipped.
Is this actually harmful? My initial thought is it's probably okay. `cpu_needs_drain` acts as a heuristic. If the count is stale (e.g., 0 instead of 1), draining *might* be skipped *this round*. But `__lru_add_drain_all` seems to have some rather intricate generation logic.
Let me clarify my understanding. If `cpu_needs_drain` returns `false`, work to drain the CPU is *not* queued. If `true`, the `lru_add_drain_per_cpu` work is queued, and this work acquires the local lock and does the actual draining.
So, if CPU Y sees a 0 when it should have seen a 1: the drain is skipped for that CPU *this time*. Is this a problem? The comments in `__lru_add_drain_all` mention `lru_drain_gen`. I need to dig into the memory barrier usage in this function.
Ah, line 877 in `__lru_add_drain_all` contains `smp_mb()`, a full memory barrier. This is a critical part, but let me break down the steps and see what barriers are in place.
If `folio_batch_add` happens on CPU X:
1. `fbatch->nr` is incremented.
2. `local_unlock` occurs (which might be a memory barrier, but it is local).
If CPU Y calls `lru_add_drain_all`:
1. `smp_mb()` (line 832).
2. `this_gen = smp_load_acquire(&lru_drain_gen)`.
3. `mutex_lock(&lock)`.
4. `WRITE_ONCE(lru_drain_gen, lru_drain_gen + 1)`.
5. `smp_mb()` (line 877).
6. `if (cpu_needs_drain(cpu))` reads `fbatch->nr`.
The question becomes: if `cpu_needs_drain` reads 0, it *missed* the update from CPU X. Does `local_unlock` in `folio_batch_add` provide sufficient guarantees of visibility across CPUs? I need to remember that `local_unlock` on certain architectures might not be a full memory barrier. However, the comments in the source indicate that `spin_unlock` is used, so it is a store-release. But `local_lock` could be different.
Wait, looking again, `local_lock` is just a `preempt_disable()` or similar in non-RT kernels. I really need to check `include/linux/local_lock.h` to see *exactly* what `local_lock` does. My current thought is that while the local locking is important for intra-CPU synchronization, it doesn't provide the inter-CPU visibility I am looking for.
|
| 14/2 |
2026/06/09 12:34 |
tool |
read-file |
0m
Args: map[File:include/linux/local_lock.h FirstLine:1 LineCount:50]
Results: map[Contents: 1: /* SPDX-License-Identifier: GPL-2.0 */
2: #ifndef _LINUX_LOCAL_LOCK_H
3: #define _LINUX_LOCAL_LOCK_H
4:
5: #include <linux/local_lock_internal.h>
6:
7: /**
8: * local_lock_init - Runtime initialize a lock instance
9: * @lock: The lock variable
10: */
11: #define local_lock_init(lock) __local_lock_init(lock)
12:
13: /**
14: * local_lock - Acquire a per CPU local lock
15: * @lock: The lock variable
16: */
17: #define local_lock(lock) __local_lock(__this_cpu_local_lock(lock))
18:
19: /**
20: * local_lock_irq - Acquire a per CPU local lock and disable interrupts
21: * @lock: The lock variable
22: */
23: #define local_lock_irq(lock) __local_lock_irq(__this_cpu_local_lock(lock))
24:
25: /**
26: * local_lock_irqsave - Acquire a per CPU local lock, save and disable
27: * interrupts
28: * @lock: The lock variable
29: * @flags: Storage for interrupt flags
30: */
31: #define local_lock_irqsave(lock, flags) \
32: __local_lock_irqsave(__this_cpu_local_lock(lock), flags)
33:
34: /**
35: * local_unlock - Release a per CPU local lock
36: * @lock: The lock variable
37: */
38: #define local_unlock(lock) __local_unlock(__this_cpu_local_lock(lock))
39:
40: /**
41: * local_unlock_irq - Release a per CPU local lock and enable interrupts
42: * @lock: The lock variable
43: */
44: #define local_unlock_irq(lock) __local_unlock_irq(__this_cpu_local_lock(lock))
45:
46: /**
47: * local_unlock_irqrestore - Release a per CPU local lock and restore
48: * interrupt flags
49: * @lock: The lock variable
50: * @flags: Interrupt flags to restore
]
|
| 15/2 |
2026/06/09 12:34 |
llm |
expert |
0m
Model:
Tokens:
input: 15172
output: 38
thoughts: 14
Thoughts: Okay, here's my interpretation of that thought, expressed as if it's coming from me, and assuming the context is my area of expertise:
**Diving into the Kernel's Locking Mechanisms**
Alright, so I'm thinking about the intricate dance of concurrency within the Linux kernel, and specifically, I'm drawn to understanding the low-level mechanics of synchronization. My mind immediately goes to the heart of the matter β the `include/linux/local_lock_internal.h` header file. This is where the real nitty-gritty of the `local_lock` implementation lies, right? It's not just about the *what*, but the *how*... the underlying data structures, the atomic operations, the subtleties of cache line management, and the potential pitfalls that need to be navigated to prevent data corruption. I need to refresh myself on how the architecture handles these local locks, especially how it fits into the broader picture of lock hierarchy and contention avoidance strategies to make informed decisions about my designs. Any optimization I'm considering is heavily influenced by understanding this core logic!
|
| 16/2 |
2026/06/09 12:35 |
tool |
read-file |
0m
Args: map[File:include/linux/local_lock_internal.h FirstLine:1 LineCount:100]
Results: map[Contents: 1: /* SPDX-License-Identifier: GPL-2.0 */
2: #ifndef _LINUX_LOCAL_LOCK_H
3: # error "Do not include directly, include linux/local_lock.h"
4: #endif
5:
6: #include <linux/percpu-defs.h>
7: #include <linux/irqflags.h>
8: #include <linux/lockdep.h>
9: #include <linux/debug_locks.h>
10: #include <asm/current.h>
11:
12: #ifndef CONFIG_PREEMPT_RT
13:
14: context_lock_struct(local_lock) {
15: #ifdef CONFIG_DEBUG_LOCK_ALLOC
16: struct lockdep_map dep_map;
17: struct task_struct *owner;
18: #endif
19: };
20: typedef struct local_lock local_lock_t;
21:
22: /* local_trylock() and local_trylock_irqsave() only work with local_trylock_t */
23: context_lock_struct(local_trylock) {
24: #ifdef CONFIG_DEBUG_LOCK_ALLOC
25: struct lockdep_map dep_map;
26: struct task_struct *owner;
27: #endif
28: u8 acquired;
29: };
30: typedef struct local_trylock local_trylock_t;
31:
32: #ifdef CONFIG_DEBUG_LOCK_ALLOC
33: # define LOCAL_LOCK_DEBUG_INIT(lockname) \
34: .dep_map = { \
35: .name = #lockname, \
36: .wait_type_inner = LD_WAIT_CONFIG, \
37: .lock_type = LD_LOCK_PERCPU, \
38: }, \
39: .owner = NULL,
40:
41: # define LOCAL_TRYLOCK_DEBUG_INIT(lockname) \
42: LOCAL_LOCK_DEBUG_INIT(lockname)
43:
44: static inline void local_lock_acquire(local_lock_t *l)
45: {
46: lock_map_acquire(&l->dep_map);
47: DEBUG_LOCKS_WARN_ON(l->owner);
48: l->owner = current;
49: }
50:
51: static inline void local_trylock_acquire(local_lock_t *l)
52: {
53: lock_map_acquire_try(&l->dep_map);
54: DEBUG_LOCKS_WARN_ON(l->owner);
55: l->owner = current;
56: }
57:
58: static inline void local_lock_release(local_lock_t *l)
59: {
60: DEBUG_LOCKS_WARN_ON(l->owner != current);
61: l->owner = NULL;
62: lock_map_release(&l->dep_map);
63: }
64:
65: static inline void local_lock_debug_init(local_lock_t *l)
66: {
67: l->owner = NULL;
68: }
69: #else /* CONFIG_DEBUG_LOCK_ALLOC */
70: # define LOCAL_LOCK_DEBUG_INIT(lockname)
71: # define LOCAL_TRYLOCK_DEBUG_INIT(lockname)
72: static inline void local_lock_acquire(local_lock_t *l) { }
73: static inline void local_trylock_acquire(local_lock_t *l) { }
74: static inline void local_lock_release(local_lock_t *l) { }
75: static inline void local_lock_debug_init(local_lock_t *l) { }
76: #endif /* !CONFIG_DEBUG_LOCK_ALLOC */
77:
78: #define INIT_LOCAL_LOCK(lockname) { LOCAL_LOCK_DEBUG_INIT(lockname) }
79: #define INIT_LOCAL_TRYLOCK(lockname) { LOCAL_TRYLOCK_DEBUG_INIT(lockname) }
80:
81: #define __local_lock_init(lock) \
82: do { \
83: static struct lock_class_key __key; \
84: \
85: debug_check_no_locks_freed((void *)lock, sizeof(*lock));\
86: lockdep_init_map_type(&(lock)->dep_map, #lock, &__key, \
87: 0, LD_WAIT_CONFIG, LD_WAIT_INV, \
88: LD_LOCK_PERCPU); \
89: local_lock_debug_init(lock); \
90: } while (0)
91:
92: #define __local_trylock_init(lock) \
93: do { \
94: __local_lock_init((local_lock_t *)lock); \
95: } while (0)
96:
97: #define __spinlock_nested_bh_init(lock) \
98: do { \
99: static struct lock_class_key __key; \
100: \
]
|
| 17/2 |
2026/06/09 12:35 |
llm |
expert |
0m
Model:
Tokens:
input: 16956
output: 39
thoughts: 17
Thoughts: Okay, here's my summary, expressed as though these were my own thoughts, given my expertise:
**Diving into the Synchronization Primitives**
Right, so I need to get my hands dirty and really understand how these local locks are implemented. Specifically, I need to drill down and examine the `__local_lock` and `__local_unlock` primitives. I know the high-level concepts, of course β the need for local synchronization when you're dealing with concurrent access to shared resources within a single machine or process is fundamental. However, I want to see *how* this is being accomplished under the hood. What mechanisms are being used? Are we talking about atomic operations, spinlocks, mutexes, or something more specialized to the platform or application? I'm especially interested in the potential tradeoffs β performance implications, potential for deadlocks or priority inversion, and how they handle contention. Seeing these specific functions will illuminate the critical path. The details are where the devil lives, and this is where the real understanding resides. So, let's see these functions!
|
| 18/2 |
2026/06/09 12:35 |
tool |
read-file |
0m
Args: map[File:include/linux/local_lock_internal.h FirstLine:110 LineCount:50]
Results: map[Contents: 110: local_trylock_t *__tl; \
111: local_lock_t *__l; \
112: \
113: __l = (local_lock_t *)(lock); \
114: __tl = (local_trylock_t *)__l; \
115: _Generic((lock), \
116: local_trylock_t *: ({ \
117: lockdep_assert(__tl->acquired == 0); \
118: WRITE_ONCE(__tl->acquired, 1); \
119: }), \
120: local_lock_t *: (void)0); \
121: local_lock_acquire(__l); \
122: } while (0)
123:
124: #define __local_lock(lock) \
125: do { \
126: preempt_disable(); \
127: __local_lock_acquire(lock); \
128: __acquire(lock); \
129: } while (0)
130:
131: #define __local_lock_irq(lock) \
132: do { \
133: local_irq_disable(); \
134: __local_lock_acquire(lock); \
135: __acquire(lock); \
136: } while (0)
137:
138: #define __local_lock_irqsave(lock, flags) \
139: do { \
140: local_irq_save(flags); \
141: __local_lock_acquire(lock); \
142: __acquire(lock); \
143: } while (0)
144:
145: #define __local_trylock(lock) \
146: __try_acquire_ctx_lock(lock, ({ \
147: local_trylock_t *__tl; \
148: \
149: preempt_disable(); \
150: __tl = (lock); \
151: if (READ_ONCE(__tl->acquired)) { \
152: preempt_enable(); \
153: __tl = NULL; \
154: } else { \
155: WRITE_ONCE(__tl->acquired, 1); \
156: local_trylock_acquire( \
157: (local_lock_t *)__tl); \
158: } \
159: !!__tl; \
]
|
| 19/2 |
2026/06/09 12:35 |
llm |
expert |
0m
Model:
Tokens:
input: 18009
output: 39
thoughts: 7
Thoughts: Okay, here's my interpretation of that thought process, tailored for an expert audience, and presented as if it were my own internal monologue:
**Thinking About Concurrency: And That Lock**
Alright, I'm digging into this code and I'm focusing on synchronization. We've got a critical section, and I'm immediately thinking about how we're protecting it. I'm seeing this `__local_unlock` macro... okay, so this implies we're working with some kind of local, perhaps per-CPU or thread-local, locking mechanism. This isn't your standard global mutex. We're optimized for contention that is constrained to a local set of resources. That's a good first clue, tells me a lot about the likely performance requirements.
It's likely that we have multiple processors in this system, so the use of "local" is in relationship to the processor it is running on. Interesting. This will impact the overall design. Now I am thinking about cache-coherency. And how the code is structured around this design choice.
I'm making a mental checklist: What's the context? How is this lock acquired, and more importantly, under what circumstances is it released? Are there any potential race conditions here with the acquire/release? Is there any contention, and how do we prevent it, other than just the lock itself? Are there any opportunities for improvement on this local locking mechanism? I'm already thinking about potential bottlenecks and how the locking strategy impacts overall system performance. I need more context to understand the specifics. But the `__local_unlock` is definitely what has me thinking about fine-grained synchronization and its implications.
|
| 20/2 |
2026/06/09 12:35 |
tool |
read-file |
0m
Args: map[File:include/linux/local_lock_internal.h FirstLine:180 LineCount:50]
Results: map[Contents: 180: #define __local_lock_is_locked(lock) READ_ONCE(this_cpu_ptr(lock)->acquired)
181:
182: #define __local_lock_release(lock) \
183: do { \
184: local_trylock_t *__tl; \
185: local_lock_t *__l; \
186: \
187: __l = (local_lock_t *)(lock); \
188: __tl = (local_trylock_t *)__l; \
189: local_lock_release(__l); \
190: _Generic((lock), \
191: local_trylock_t *: ({ \
192: lockdep_assert(__tl->acquired == 1); \
193: WRITE_ONCE(__tl->acquired, 0); \
194: }), \
195: local_lock_t *: (void)0); \
196: } while (0)
197:
198: #define __local_unlock(lock) \
199: do { \
200: __release(lock); \
201: __local_lock_release(lock); \
202: preempt_enable(); \
203: } while (0)
204:
205: #define __local_unlock_irq(lock) \
206: do { \
207: __release(lock); \
208: __local_lock_release(lock); \
209: local_irq_enable(); \
210: } while (0)
211:
212: #define __local_unlock_irqrestore(lock, flags) \
213: do { \
214: __release(lock); \
215: __local_lock_release(lock); \
216: local_irq_restore(flags); \
217: } while (0)
218:
219: #define __local_lock_nested_bh(lock) \
220: do { \
221: lockdep_assert_in_softirq(); \
222: local_lock_acquire((lock)); \
223: __acquire(lock); \
224: } while (0)
225:
226: #define __local_unlock_nested_bh(lock) \
227: do { \
228: __release(lock); \
229: local_lock_release((lock)); \
]
|
| 21/2 |
2026/06/09 12:35 |
llm |
expert |
0m
Model:
Tokens:
input: 19186
output: 33
thoughts: 1081
Thoughts: **Analyzing a Potential Data Race in LRU Page Draining**
Okay, let's break this down. On a non-real-time kernel, `local_lock` is essentially `preempt_disable()` and `local_unlock` is `preempt_enable()`. The latter might have a barrier if it calls `preempt_schedule()`, but otherwise, it's just a preemption count decrement. `local_irq_restore` (used by `local_unlock_irqrestore` on x86) has some ordering properties, but it's *not* a full memory barrier. This isn't where the core issue lies, though.
The reported race is on `fbatch->nr`, the page count in a per-CPU folio batch. The core problem is that `cpu_needs_drain` might see a stale value of 0, and not drain the batch. The real kicker is that `__lru_add_drain_all` is called when we absolutely *need* to be sure that all pages are actually on the LRU lists, for example during reclaim or (as in this stack trace) when invalidating a block device via `invalidate_bdev`.
If `invalidate_bdev` calls `lru_add_drain_all`, it expects that when it returns, *all* the pages that were in per-CPU batches have been processed. If there's a race and `cpu_needs_drain` misses a batch due to a stale read, `invalidate_bdev` proceeds *assuming* everything is done, but some pages are still in flight. This could leave pages undrained, potentially leading to incorrect page invalidation.
Now, that `lru_drain_gen` variable in `__lru_add_drain_all` is supposed to help, with its memory barriers. It's supposed to ensure proper ordering, with the writer of the `lru_drain_gen` and `folio_batch_add`.
I'm thinking through this as I review the code, especially that `WRITE_ONCE` and `smp_mb()` usage, plus the `smp_load_acquire` and `this_gen` load. I need to be sure the memory ordering constraints are correct so that the batches are drained as expected.
Essentially, there's a paired barrier operation, which I believe I understand. The intention is to ensure that when a CPU, let's say CPU X, adds a page and *then* calls `lru_add_drain_all()`, it sees the latest `lru_drain_gen` value, and not miss flushing all pages. So a CPU Z increments it, and potentially CPU X might see the new value. However, the comments in the code, I believe, are trying to highlight an edge case: What if CPU Z is *past* CPU X in the loop? Then CPU X would see the new generation and bail, and miss its pages.
The race in the report is between `folio_batch_add` and `cpu_needs_drain`. So, if `folio_batch_add` is on CPU 0 and `__lru_add_drain_all` is on CPU 1. Then, CPU 1 will call `cpu_needs_drain(0)`. If `cpu_needs_drain(0)` returns false because it sees `nr == 0`, CPU 1 won't drain CPU 0. If CPU 0 added the page and it then called `lru_add_drain_all()`, or another task did, then we would have this state.
In this specific report, both tasks are calling `invalidate_bdev` -> `lru_add_drain_all`. The stack trace reveals that a task is in `deactivate_file_folio`, called by `mapping_try_invalidate`, called by `invalidate_mapping_pages`, which is called by `invalidate_bdev`. It's crucial, and the bug lies in understanding that `invalidate_bdev` *first* calls `lru_add_drain_all`, then `invalidate_bh_lrus`, *then* `invalidate_mapping_pages`. This is significant! This could indeed lead to the issues described. I should consult `block/bdev.c` for more info.
|
| 22/2 |
2026/06/09 12:35 |
tool |
read-file |
0m
Args: map[File:block/bdev.c FirstLine:90 LineCount:20]
Results: map[Contents: 90: invalidate_bh_lrus();
91: truncate_inode_pages(mapping, 0);
92: }
93:
94: /* Invalidate clean unused buffers and pagecache. */
95: void invalidate_bdev(struct block_device *bdev)
96: {
97: struct address_space *mapping = bdev->bd_mapping;
98:
99: if (mapping->nrpages) {
100: invalidate_bh_lrus();
101: lru_add_drain_all(); /* make sure all lru add caches are flushed */
102: invalidate_mapping_pages(mapping, 0, -1);
103: }
104: }
105: EXPORT_SYMBOL(invalidate_bdev);
106:
107: /*
108: * Drop all buffers & page cache for given bdev range. This function bails
109: * with error if bdev has other exclusive owner (such as filesystem).
]
|
| 23/2 |
2026/06/09 12:35 |
llm |
expert |
0m
Model:
Tokens:
input: 21517
output: 33
thoughts: 217
Thoughts: **Analyzing Task 3307's Invalidation Process**
Okay, so I'm looking at Task 3306, which is calling `lru_add_drain_all()` on line 101, and right after that, on line 102, Task 3307 is calling `invalidate_mapping_pages(mapping, 0, -1)`. But hold on, the stack trace for 3307 is really interesting. It shows that 3307 is *already* inside `invalidate_mapping_pages`. The trace goes: `deactivate_file_folio`, then `mapping_try_invalidate` which then calls `invalidate_mapping_pages`, which then calls `invalidate_bdev`. So it looks like 3307 is already inside `invalidate_mapping_pages` and is likely calling `mapping_try_invalidate`. I should probably check out `mm/truncate.c` to see how it's being used.
|
| 24/2 |
2026/06/09 12:35 |
tool |
read-file |
0m
Args: map[File:mm/truncate.c FirstLine:550 LineCount:60]
Results: map[Contents: 550: struct folio *folio = fbatch.folios[i];
551:
552: /* We rely upon deletion not changing folio->index */
553:
554: if (xa_is_value(folio)) {
555: xa_has_values = true;
556: count++;
557: continue;
558: }
559:
560: ret = mapping_evict_folio(mapping, folio);
561: folio_unlock(folio);
562: /*
563: * Invalidation is a hint that the folio is no longer
564: * of interest and try to speed up its reclaim.
565: */
566: if (!ret) {
567: deactivate_file_folio(folio);
568: /* Likely in the lru cache of a remote CPU */
569: if (nr_failed)
570: (*nr_failed)++;
571: }
572: count += ret;
573: }
574:
575: if (xa_has_values)
576: clear_shadow_entries(mapping, indices[0], indices[nr-1]);
577:
578: folio_batch_remove_exceptionals(&fbatch);
579: folio_batch_release(&fbatch);
580: cond_resched();
581: }
582: return count;
583: }
584:
585: /**
586: * invalidate_mapping_pages - Invalidate all clean, unlocked cache of one inode
587: * @mapping: the address_space which holds the cache to invalidate
588: * @start: the offset 'from' which to invalidate
589: * @end: the offset 'to' which to invalidate (inclusive)
590: *
591: * This function removes pages that are clean, unmapped and unlocked,
592: * as well as shadow entries. It will not block on IO activity.
593: *
594: * If you want to remove all the pages of one inode, regardless of
595: * their use and writeback state, use truncate_inode_pages().
596: *
597: * Return: The number of indices that had their contents invalidated
598: */
599: unsigned long invalidate_mapping_pages(struct address_space *mapping,
600: pgoff_t start, pgoff_t end)
601: {
602: return mapping_try_invalidate(mapping, start, end, NULL);
603: }
604: EXPORT_SYMBOL(invalidate_mapping_pages);
605:
606: static int folio_launder(struct address_space *mapping, struct folio *folio)
607: {
608: if (!folio_test_dirty(folio))
609: return 0;
]
|
| 25/2 |
2026/06/09 12:35 |
llm |
expert |
0m
Model:
Tokens:
input: 22829
output: 33
thoughts: 76
Thoughts: Okay, here's my breakdown of what's going on.
**Analyzing Folio Eviction Failure**
Right, so `deactivate_file_folio` is being called. I see it's triggered because `mapping_evict_folio` failed. Wait a minuteβ¦ is that right? `!ret` in this context means the eviction *didn't* happen? Need to be sure here. Okay, let's step back and double-check how `mapping_evict_folio` works again β just to clarify. That function's return value is crucial to understanding this flow. Let's dig into `mapping_evict_folio` and confirm its return codes. I need to be completely certain about that logic before moving forward.
|
| 26/2 |
2026/06/09 12:35 |
tool |
read-file |
0m
Args: map[File:mm/truncate.c FirstLine:250 LineCount:20]
Results: map[Contents: 250: if (!folio_test_large(folio))
251: return true;
252:
253: min_order = mapping_min_folio_order(folio->mapping);
254: split_at = folio_page(folio, PAGE_ALIGN_DOWN(offset) / PAGE_SIZE);
255: if (!try_folio_split_or_unmap(folio, split_at, min_order)) {
256: /*
257: * try to split at offset + length to make sure folios within
258: * the range can be dropped, especially to avoid memory waste
259: * for shmem truncate
260: */
261: struct folio *folio2;
262:
263: if (offset + length == size)
264: goto no_split;
265:
266: split_at2 = folio_page(folio,
267: PAGE_ALIGN_DOWN(offset + length) / PAGE_SIZE);
268: folio2 = page_folio(split_at2);
269:
]
|
| 27/2 |
2026/06/09 12:35 |
llm |
expert |
0m
Model:
Tokens:
input: 23417
output: 20
thoughts: 26
Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience:
**Debugging a Mapping Issue**
Hold on a second, I think I've gone down the wrong rabbit hole. I'm focusing on the wrong instance of `mapping_evict_folio`. Clearly, this isn't the one I was intending to analyze. I need to backtrack and pinpoint the *actual* definition of `mapping_evict_folio` that's relevant to this specific problem. I need to understand its context within this module, and that probably means tracing its definition and any relevant data structures. This is a common debugging step β a misdirection. Gotta zero in on the *correct* object to proceed effectively.
|
| 28/2 |
2026/06/09 12:35 |
tool |
grepper |
0m
Args: map[Expression:mapping_evict_folio]
Results: map[Output:mm/internal.h=561=bool truncate_inode_partial_folio(struct folio *folio, loff_t start,
mm/internal.h-562- loff_t end);
mm/internal.h:563:long mapping_evict_folio(struct address_space *mapping, struct folio *folio);
mm/internal.h-564-unsigned long mapping_try_invalidate(struct address_space *mapping,
--
mm/memory-failure.c=935=static int truncate_error_folio(struct folio *folio, unsigned long pfn,
--
mm/memory-failure.c-953- */
mm/memory-failure.c:954: if (mapping_evict_folio(mapping, folio))
mm/memory-failure.c-955- ret = MF_RECOVERED;
--
mm/memory-failure.c=2791=static int soft_offline_in_use_page(struct page *page)
--
mm/memory-failure.c-2838- */
mm/memory-failure.c:2839: ret = mapping_evict_folio(folio_mapping(folio), folio);
mm/memory-failure.c-2840- folio_unlock(folio);
--
mm/memory.c=5447=static vm_fault_t __do_fault(struct vm_fault *vmf)
--
mm/memory.c-5485- /* Retry if a clean folio was removed from the cache. */
mm/memory.c:5486: if (mapping_evict_folio(folio->mapping, folio))
mm/memory.c-5487- poisonret = VM_FAULT_NOPAGE;
--
mm/truncate.c=312=EXPORT_SYMBOL(generic_error_remove_folio);
--
mm/truncate.c-314-/**
mm/truncate.c:315: * mapping_evict_folio() - Remove an unused folio from the page-cache.
mm/truncate.c-316- * @mapping: The mapping this folio belongs to.
--
mm/truncate.c-324- */
mm/truncate.c:325:long mapping_evict_folio(struct address_space *mapping, struct folio *folio)
mm/truncate.c-326-{
--
mm/truncate.c=534=unsigned long mapping_try_invalidate(struct address_space *mapping,
--
mm/truncate.c-559-
mm/truncate.c:560: ret = mapping_evict_folio(mapping, folio);
mm/truncate.c-561- folio_unlock(folio);
--
mm/truncate.c=606=static int folio_launder(struct address_space *mapping, struct folio *folio)
--
mm/truncate.c-615-/*
mm/truncate.c:616: * This is like mapping_evict_folio(), except it ignores the folio's
mm/truncate.c-617- * refcount. We do this because invalidate_inode_pages2() needs stronger
]
|
| 29/2 |
2026/06/09 12:35 |
llm |
expert |
0m
Model:
Tokens:
input: 24380
output: 33
thoughts: 13
Thoughts: Okay, **Let's Break Down This Snippet!**
Right, so I'm looking at `mm/truncate.c:325`. Immediately, my expert brain kicks in and I'm sifting through the context, assuming this is kernel memory management code. Line 325. Okay, `mm/truncate.c` strongly suggests we are dealing with a filesystem operation, specifically concerning file truncation, likely freeing up blocks that are no longer needed, perhaps as the file size is being reduced.
I'm picturing the function call stack now. I'm immediately thinking about how this relates to inode operations, the underlying block device, and potential interactions with the page cache. Is this related to a direct IO path or a buffered IO path? The presence of 'truncate' strongly suggests we're dealing with potentially releasing space. My brain starts considering concurrent access scenarios. Are we handling locking properly to protect against race conditions with other processes also trying to modify the file? What about journaled filesystems? Does the journaling handle the metadata updates and block deallocation safely here?
And the exact location in the file? I want to know, is this the start of the truncation process or the end. Is this where the blocks are freed? Is this a part of the locking sequence? Are we cleaning up any dirty pages? Is there a possibility that this line is the place where all the old file blocks are zeroed? I instantly want to consider the context of the nearby lines to fully understand its function. I'll need to drill down deeper into the surrounding code to determine if itβs an early check, a critical update, or a completion handler. My suspicion is this line may be a pivotal point in a complex sequence of operations.
|
| 30/2 |
2026/06/09 12:35 |
tool |
read-file |
0m
Args: map[File:mm/truncate.c FirstLine:325 LineCount:20]
Results: map[Contents: 325: long mapping_evict_folio(struct address_space *mapping, struct folio *folio)
326: {
327: /* The page may have been truncated before it was locked */
328: if (!mapping)
329: return 0;
330: if (folio_test_dirty(folio) || folio_test_writeback(folio))
331: return 0;
332: /* The refcount will be elevated if any page in the folio is mapped */
333: if (folio_ref_count(folio) >
334: folio_nr_pages(folio) + folio_has_private(folio) + 1)
335: return 0;
336: if (!filemap_release_folio(folio, 0))
337: return 0;
338:
339: return remove_mapping(mapping, folio);
340: }
341:
342: /**
343: * truncate_inode_pages_range - truncate range of pages specified by start & end byte offsets
344: * @mapping: mapping to truncate
]
|
| 31/2 |
2026/06/09 12:35 |
llm |
expert |
0m
Model:
Tokens:
input: 25114
output: 36
thoughts: 760
Thoughts: **Analyzing the Potential Race Condition in Folio Deactivation**
Okay, let's break this down. `mapping_evict_folio` returns 1 for success, 0 for failure. In `mapping_try_invalidate`, if the folio isn't evicted β maybe it's dirty, undergoing writeback, or has extra references β we call `deactivate_file_folio`. This is a "hint," a way to nudge the folio towards the inactive LRU list using a per-CPU batch.
The core concern here is the race. We have the folio being added to this batch, and then a separate process, `lru_add_drain_all`, checking if this batch needs to be drained. If `lru_add_drain_all` misses the addition, the folio stays in the batch until the next drain. It's not a correctness issue for `invalidate_mapping_pages`, since the folio wasn't evicted anyway.
The plot thickens with `invalidate_bdev`. This routine calls `lru_add_drain_all` *before* `invalidate_mapping_pages`. My attention is drawn to the stack trace. Task 3306 is indeed calling `lru_add_drain_all` from `invalidate_bdev`. Task 3307 is already in `invalidate_mapping_pages`, which follows in `invalidate_bdev`. So Task 3307 might be dealing with folios that Task 3306 (or another task) might have missed in its `lru_add_drain_all` call?
But wait! Both tasks, 3307 and 3306, are executing `ext4_put_super` -> `invalidate_bdev`. They're probably unmounting the same filesystem! The log confirms: "EXT4-fs (loop4): unmounting filesystem..." If two tasks are unmounting the same filesystem, they might both be calling `invalidate_bdev` which calls `lru_add_drain_all` and then `invalidate_mapping_pages`.
Now, the potential race on `fbatch->nr` in `folio_batch_add` vs `folio_batch_count`. In `folio_batch_add`, `fbatch->nr++` isn't atomic. It involves a read, store, increment, and then a final store. `folio_batch_count` just reads `fbatch->nr`.
This means `folio_batch_count` could read: the old value, the new value, or a torn value (a partial update) if `nr` were larger than 1 byte. The report mentions a "read-write ... of 1 bytes" and "read ... of 1 bytes". Aha! `fbatch->nr` must be an `unsigned char`! That explains the 1-byte read-write. Given that `fbatch->nr` is a single byte, it is possible for a read to occur between the first store and the increment, therefore reading the wrong number.
|
| 32/2 |
2026/06/09 12:35 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:include/linux/folio_batch.h Name:folio_batch]
Results: map[SourceCode: 19: /**
20: * struct folio_batch - A collection of folios.
21: *
22: * The folio_batch is used to amortise the cost of retrieving and
23: * operating on a set of folios. The order of folios in the batch may be
24: * significant (eg delete_from_page_cache_batch()). Some users of the
25: * folio_batch store "exceptional" entries in it which can be removed
26: * by calling folio_batch_remove_exceptionals().
27: */
28: struct folio_batch {
29: unsigned char nr;
30: unsigned char i;
31: bool percpu_pvec_drained;
32: struct folio *folios[FOLIO_BATCH_SIZE];
33: };
SourceFile:include/linux/folio_batch.h]
|
| 33/2 |
2026/06/09 12:35 |
llm |
expert |
0m
Model:
Tokens:
input: 26813
output: 38
thoughts: 1413
Thoughts: **Analyzing Potential Races in LRU Batch Draining**
Okay, so `nr` is an `unsigned char`, meaning it's just one byte. No problems there with partial reads or writes.
The core of the problem seems to be visibility between CPUs. If a reader sees the old value of `nr` (0), it thinks the batch is empty and doesn't drain. If it sees the new value (1), it thinks something's there and drains. The question is, can the reader see `nr == 1` before it sees the actual folio data stored in `folios[0]`?
`folio_batch_add` does the following, critically:
```c
fbatch->folios[fbatch->nr++] = folio;
```
It's likely that a compiler could reorder the increment of `nr` before storing the `folio`, but the critical bit is that `nr++` uses the *old* value as the index. Therefore it should be thought of as:
```c
tmp = fbatch->nr;
fbatch->nr = tmp + 1;
fbatch->folios[tmp] = folio;
```
If the compiler reorders, the reader could see `nr == 1` but `folios[0]` uninitialized or with stale data. However, the `cpu_needs_drain` function only checks `nr`. It doesn't inspect the `folios[]` array directly.
The actual drain happens in `lru_add_drain_per_cpu` which is work-item on the target CPU, and this is where synchronization matters. If that drain is run on the same CPU as the store, it'll see the local stores, and the `local_lock` guarantees visibility. The harm here is only in delayed drain, not incorrect data.
A delayed drain might be problematic in the `invalidate_bdev` context where we want to ensure everything is flushed. If we miss a page in `lru_add_drain_all`, `invalidate_mapping_pages` could miss it too, if it's still in the `lru_add` batch. But then, if it is in the `lru_add` batch, it's not on the LRU yet, but it *is* in the page cache. `invalidate_mapping_pages` iterates the page cache using `filemap_get_folios` and so it will find the folio.
If the folio is in the `lru_add` batch, that means it's recently been added to the page cache. `deactivate_file_folio` is called when we fail to evict, so missing the drain just means the folio stays on the active list a bit longer. Definitely benign.
What about `lru_add` batches? If a folio is in one, it's in the page cache but not on an LRU. `invalidate_mapping_pages` will find it and try to evict it. If it's in a per-CPU batch, it has an extra reference from that batch. `folio_batch_add` calls `folio_get(folio)`. So `folio_ref_count` will be higher, and `mapping_evict_folio` will fail to evict it. The relevant condition for the eviction failure is:
```c
if (folio_ref_count(folio) >
folio_nr_pages(folio) + folio_has_private(folio) + 1)
return 0;
```
Here, the `+ 1` accounts for the caller's reference. So, the extra reference from the per-CPU batch would cause an eviction failure.
So, missing a CPU's batch during `lru_add_drain_all` might mean folios aren't evicted by `invalidate_mapping_pages` because they're held in per-CPU batches. That's why `invalidate_bdev` calls `lru_add_drain_all` in the first place. Therefore, `invalidate_bdev` might be less effective if the drain isn't guaranteed to cover all additions made *before* the call.
But then there's the `smp_mb()` calls in `__lru_add_drain_all`:
```c
/*
* Guarantee folio_batch counter stores visible by this CPU
* are visible to other CPUs before loading the current drain
* generation.
*/
smp_mb();
```
That `smp_mb()` at the *beginning* of `__lru_add_drain_all` ensures that stores *by the current CPU* are visible, but not necessarily stores from *other* CPUs. The generation logic is supposed to handle multiple CPUs calling `
|
| 34/2 |
2026/06/09 12:36 |
tool |
read-file |
0m
Args: map[File:fs/ext4/super.c FirstLine:1300 LineCount:100]
Results: map[Contents:1300:
1301: if (___ratelimit(&ext4_mount_msg_ratelimit, "EXT4-fs unmount"))
1302: ext4_msg(sb, KERN_INFO, "unmounting filesystem %pU.",
1303: &sb->s_uuid);
1304:
1305: ext4_unregister_li_request(sb);
1306: ext4_quotas_off(sb, EXT4_MAXQUOTAS);
1307:
1308: destroy_workqueue(sbi->rsv_conversion_wq);
1309: ext4_release_orphan_info(sb);
1310:
1311: if (sbi->s_journal) {
1312: aborted = is_journal_aborted(sbi->s_journal);
1313: err = ext4_journal_destroy(sbi, sbi->s_journal);
1314: if ((err < 0) && !aborted) {
1315: ext4_abort(sb, -err, "Couldn't clean up the journal");
1316: }
1317: } else
1318: flush_work(&sbi->s_sb_upd_work);
1319:
1320: ext4_es_unregister_shrinker(sbi);
1321: timer_shutdown_sync(&sbi->s_err_report);
1322: ext4_release_system_zone(sb);
1323: ext4_mb_release(sb);
1324: ext4_ext_release(sb);
1325:
1326: if (!ext4_emergency_state(sb) && !sb_rdonly(sb)) {
1327: if (!aborted) {
1328: ext4_clear_feature_journal_needs_recovery(sb);
1329: ext4_clear_feature_orphan_present(sb);
1330: es->s_state = cpu_to_le16(sbi->s_mount_state);
1331: }
1332: ext4_commit_super(sb);
1333: }
1334:
1335: ext4_group_desc_free(sbi);
1336: ext4_flex_groups_free(sbi);
1337:
1338: WARN_ON_ONCE(!(sbi->s_mount_state & EXT4_ERROR_FS) &&
1339: percpu_counter_sum(&sbi->s_dirtyclusters_counter));
1340: ext4_percpu_param_destroy(sbi);
1341: #ifdef CONFIG_QUOTA
1342: for (int i = 0; i < EXT4_MAXQUOTAS; i++)
1343: kfree(get_qf_name(sb, sbi, i));
1344: #endif
1345:
1346: /* Debugging code just in case the in-memory inode orphan list
1347: * isn't empty. The on-disk one can be non-empty if we've
1348: * detected an error and taken the fs readonly, but the
1349: * in-memory list had better be clean by this point. */
1350: if (!list_empty(&sbi->s_orphan))
1351: dump_orphan_list(sb, sbi);
1352: ASSERT(list_empty(&sbi->s_orphan));
1353:
1354: sync_blockdev(sb->s_bdev);
1355: invalidate_bdev(sb->s_bdev);
1356: if (sbi->s_journal_bdev_file) {
1357: /*
1358: * Invalidate the journal device's buffers. We don't want them
1359: * floating about in memory - the physical journal device may
1360: * hotswapped, and it breaks the `ro-after' testing code.
1361: */
1362: sync_blockdev(file_bdev(sbi->s_journal_bdev_file));
1363: invalidate_bdev(file_bdev(sbi->s_journal_bdev_file));
1364: }
1365:
1366: ext4_xattr_destroy_cache(sbi->s_ea_inode_cache);
1367: sbi->s_ea_inode_cache = NULL;
1368:
1369: ext4_xattr_destroy_cache(sbi->s_ea_block_cache);
1370: sbi->s_ea_block_cache = NULL;
1371:
1372: ext4_stop_mmpd(sbi);
1373:
1374: brelse(sbi->s_sbh);
1375: sb->s_fs_info = NULL;
1376: /*
1377: * Now that we are completely done shutting down the
1378: * superblock, we need to actually destroy the kobject.
1379: */
1380: kobject_put(&sbi->s_kobj);
1381: wait_for_completion(&sbi->s_kobj_unregister);
1382: kfree(sbi->s_blockgroup_lock);
1383: fs_put_dax(sbi->s_daxdev, NULL);
1384: fscrypt_free_dummy_policy(&sbi->s_dummy_enc_policy);
1385: #if IS_ENABLED(CONFIG_UNICODE)
1386: utf8_unload(sb->s_encoding);
1387: #endif
1388: kfree(sbi);
1389: }
1390:
1391: static struct kmem_cache *ext4_inode_cachep;
1392:
1393: /*
1394: * Called inside transaction, so use GFP_NOFS
1395: */
1396: static struct inode *ext4_alloc_inode(struct super_block *sb)
1397: {
1398: struct ext4_inode_info *ei;
1399:
]
|
| 35/2 |
2026/06/09 12:36 |
llm |
expert |
0m
Model:
Tokens:
input: 31001
output: 33
thoughts: 1399
Thoughts: **Analyzing a Potential Race Condition in Folio Deactivation**
Okay, here's what I'm thinking... We've got this scenario where folios associated with `sb->s_bdev->bd_mapping` are being deactivated, and there's a potential race condition happening during the `invalidate_bdev` process. It seems Task 3307 is in `deactivate_file_folio`, which is happening because `invalidate_mapping_pages` couldn't evict a folio. Meanwhile, Task 3306 is also in `invalidate_bdev` but at the `lru_add_drain_all()` call.
The core of the issue boils down to a race between `folio_batch_add` (on one CPU) and `cpu_needs_drain` (on another CPU). The crux is this: If `cpu_needs_drain` misses an update to a folio batch, the folio could remain in the batch longer than intended. `invalidate_bdev` is trying to flush any folios that *were* already in batches. If `lru_add_drain_all` doesn't pick up a "new" folio, added during or after `lru_add_drain_all` but before `cpu_needs_drain`, then the folio won't be flushed.
Now, Task 3307 is *already* in `invalidate_mapping_pages`, in a state where deactivating a folio after eviction fails; missing the drain is harmless in that case. Deactivating just moves the folio to the inactive LRU list, which isn't the same as an eviction.
If `invalidate_bdev` is called, we ideally want to evict pages. `lru_add_drain_all` is supposed to flush them. But if `lru_add_drain_all` misses something added just *before* it starts, then it's a classic race, isn't it? The kernel usually considers these kinds of race conditions on stats or heuristics "benign," provided they don't lead to crashes or bad logic that's irrecoverable. The worst case here is a folio staying in the batch a bit longer.
However, KCSAN is reporting it, which warrants deeper analysis. I'm focusing on "fused accesses" or "torn reads." Torn reads aren't a concern here, because we're only dealing with a single byte. Looking at the `cpu_needs_drain` loop, the compiler isn't going to hoist the read of `fbatch->nr` out of the loop because the `cpu` variable is changing. We're reading `per_cpu` data, and it's reading `per_cpu(cpu_fbatches, 0).lru_add.nr`, then `per_cpu(cpu_fbatches, 1).lru_add.nr`, etc. The compiler has no business hoisting.
Regarding the writer, `fbatch->nr++` is a read-modify-write. On x86, an `incb` instruction on memory isn't atomic unless it's `lock`-prefixed. But `folio_batch_add` is protected by `local_lock`, so there's no concurrent writer *on the same* CPU. The only potential concurrency is from a remote reader.
So, if the remote reader sees a partially updated value of `nr`, is that a problem? Since it's a byte, there's no such thing as a "partial" update. It will be the old or the new value, that is fine.
The only possible reordering would be the increment to `nr` and the write to `folios[nr]`. If the increment to `nr` happens first, the reader would see a value for `nr == 1`. However, the reader doesn't even look at the contents of `folios[0]`, so the race is harmless.
In conclusion, I think this is a benign race condition, but it's good that KCSAN flagged it. It might be worth a comment to emphasize this to any future maintainers.
|
| 36/2 |
2026/06/09 12:36 |
tool |
read-file |
0m
Args: map[File:mm/swap.c FirstLine:630 LineCount:50]
Results: map[Contents: 630: folio_clear_referenced(folio);
631: /*
632: * Lazyfree folios are clean anonymous folios. They have
633: * the swapbacked flag cleared, to distinguish them from normal
634: * anonymous folios
635: */
636: folio_clear_swapbacked(folio);
637: lruvec_add_folio(lruvec, folio);
638:
639: __count_vm_events(PGLAZYFREE, nr_pages);
640: count_memcg_events(lruvec_memcg(lruvec), PGLAZYFREE, nr_pages);
641: }
642:
643: /*
644: * Drain pages out of the cpu's folio_batch.
645: * Either "cpu" is the current CPU, and preemption has already been
646: * disabled; or "cpu" is being hot-unplugged, and is already dead.
647: */
648: void lru_add_drain_cpu(int cpu)
649: {
650: struct cpu_fbatches *fbatches = &per_cpu(cpu_fbatches, cpu);
651: struct folio_batch *fbatch = &fbatches->lru_add;
652:
653: if (folio_batch_count(fbatch))
654: folio_batch_move_lru(fbatch, lru_add);
655:
656: fbatch = &fbatches->lru_move_tail;
657: /* Disabling interrupts below acts as a compiler barrier. */
658: if (data_race(folio_batch_count(fbatch))) {
659: unsigned long flags;
660:
661: /* No harm done if a racing interrupt already did this */
662: local_lock_irqsave(&cpu_fbatches.lock_irq, flags);
663: folio_batch_move_lru(fbatch, lru_move_tail);
664: local_unlock_irqrestore(&cpu_fbatches.lock_irq, flags);
665: }
666:
667: fbatch = &fbatches->lru_deactivate_file;
668: if (folio_batch_count(fbatch))
669: folio_batch_move_lru(fbatch, lru_deactivate_file);
670:
671: fbatch = &fbatches->lru_deactivate;
672: if (folio_batch_count(fbatch))
673: folio_batch_move_lru(fbatch, lru_deactivate);
674:
675: fbatch = &fbatches->lru_lazyfree;
676: if (folio_batch_count(fbatch))
677: folio_batch_move_lru(fbatch, lru_lazyfree);
678:
679: folio_activate_drain(cpu);
]
|
| 37/2 |
2026/06/09 12:36 |
llm |
expert |
0m
Model:
Tokens:
input: 34175
output: 37
thoughts: 713
Thoughts: **Analyzing the LRU Drainer Logic**
Okay, let's break this down. My initial observation was about the interaction between the LRU drainer and writers. It looks like `lru_add_drain_cpu` is called by `lru_add_drain_per_cpu`, which in turn, is a work function. `lru_add_drain()` acquires the `local_lock` before calling `lru_add_drain_cpu` on the *same* CPU as the writer. Since both the drainer and the writer use the same `local_lock`, it means they're mutually exclusive. This guarantees the drainer sees all the stores made by the writer, which is what we expect.
The only potential hiccup is `cpu_needs_drain`, which runs on a *remote* CPU and determines whether to queue the drainer. If it checks the `nr` value of a batch and sees `0` when it should be `1`, the drainer isn't queued. This would just be a delayed drain, not a data race, per se.
However, I stumbled upon something interesting within `lru_add_drain_cpu`. It uses `data_race(folio_batch_count(fbatch))` at line 658! Why only for `lru_move_tail`? And why the `data_race` designation? My hypothesis is that `lru_move_tail` *can* be modified from interrupt context, specifically via `folio_rotate_reclaimable` within `end_page_writeback`, which can be called in an IRQ context. But `folio_rotate_reclaimable` *does* call `folio_batch_add_and_move` with `disable_irq = true`.
So, here's the scenario: Imagine `lru_add_drain_cpu` running in a task context, and then an interrupt fires that adds something to `lru_move_tail`. The `data_race` check is a fast path to avoid taking the lock if the batch is empty.
But here's the kicker: `cpu_needs_drain` isn't using `data_race()`! It's checking batches on *remote* CPUs. We need to go back and examine the history of `cpu_needs_drain` to fully understand what's happening.
|
| 38/2 |
2026/06/09 12:36 |
tool |
git-log |
0m
Error: SymbolName and PathPrefix cannot be used together
Args: map[Count:10 PathPrefix:mm/swap.c SymbolName:cpu_needs_drain]
Results:
|
| 39/2 |
2026/06/09 12:36 |
llm |
expert |
0m
Model:
Tokens:
input: 35454
output: 26
thoughts: 3
Thoughts: Okay, I'm ready. Here's how I'll summarize the text, assuming I'm an expert in the field and the text is, well, my own internal monologue:
**My Internal Processing & Response**
My immediate reaction is to offer a sincere apology. I understand that the original intention of what was being expressed might not be entirely clear. It might have been clumsily presented, or perhaps I'm anticipating some confusion or potential for a misunderstanding of some sort. Therefore, my first instinct is to preemptively acknowledge any possibility of a miscommunication and apologize in advance.
|
| 40/2 |
2026/06/09 12:36 |
tool |
git-log |
0m
Error: SourcePath is required when SymbolName is set
Args: map[Count:10 SymbolName:cpu_needs_drain]
Results:
|
| 41/2 |
2026/06/09 12:36 |
llm |
expert |
0m
Model:
Tokens:
input: 35628
output: 37
thoughts: 9
Thoughts: Here's your summary:
**The Frustration of Inadequate Tools**
Okay, so I'm hitting a wall here. I'm supposed to be leveraging these new tools, and frankly, I'm justβ¦failing. It's not a matter of the underlying concepts; I *get* the theory, the principles are sound. But the implementation? The interface? The way these tools are designed to translate my mental models into actual results? It's just not clicking. I'm wrestling with them, fighting the system rather than using it. I'm finding myself focusing more on the mechanics of *using* the tools than on the core problem I'm trying to solve, and that's a serious red flag. I know I'm supposed to be efficient, to be accelerating my workflow, but right now, I'm spending more time troubleshooting than creating. It's frustrating. It feels like a fundamental disconnect between my expertise and the supposed "ease" these tools promised. Time to step back, re-evaluate, maybe even go back to basics for a bit, and really understand what's not working here.
|
| 42/2 |
2026/06/09 12:36 |
tool |
git-log |
0m
Args: map[Count:10 SourcePath:mm/swap.c SymbolName:cpu_needs_drain]
Results: map[Output:2f52c77128b1 mm/swap: fold lru_rotate into cpu_fbatches
380d70549301 mm/swap: rename cpu_fbatches->activate
96f97c438f61 mm: mlock: update the interface to use folios
4864545a4669 mm/swap: pull the CPU conditional out of __lru_add_drain_all()
9852a7212324 mm: drop hotplug lock from lru_add_drain_all()
a47fed5b5b01 mm: swap: provide lru_add_drain_all_cpuslocked()
5fbc461636c3 mm: make lru_add_drain_all() selective
053837fce7aa [PATCH] mm: migration page refcounting fix
]
|
| 43/2 |
2026/06/09 12:36 |
llm |
expert |
0m
Model:
Tokens:
input: 36138
output: 26
thoughts: 32
Thoughts: Okay, here's my take on those thoughts, framed as a first-person expert summary:
**Analyzing the Selective `lru_add_drain_all()` Implementation**
Alright, let's dive into that commit, `5fbc461636c3`. My primary interest here is how it changed the behavior of `lru_add_drain_all()`, specifically making it selective. I need to understand what triggered the need for selectivity. Was it performance? Perhaps to avoid unnecessary operations under certain conditions? Or maybe, there were correctness concerns that required a more targeted approach?
I need to quickly grasp the implications of this change on the overall LRU (Least Recently Used) cache strategy. Has this created any potential for starvation? Are there now more complex edge cases that need to be considered in terms of cache coherency or resource contention? I'll probably have to dig into the surrounding code β the callsites of `lru_add_drain_all()` before and after this commit, to get the full picture.
I want to see what criteria the selector is using. What signals that function is now looking at to decide whether or not to drain the cache. Was it size, age, or something else entirely? A quick check should tell me that. I need to be sure that the selectivity is well-defined and that the rationale is solid. Any selective approach should improve performance, or reliability, not compromise it. I'm going to look for any comments in the code and potentially cross reference any relevant issue tickets.
|
| 44/2 |
2026/06/09 12:36 |
tool |
git-show |
0m
Args:
Results: map[Output:commit 5fbc461636c32efdb9d5216d491d37a40d54535b
Author: Chris Metcalf <cmetcalf@tilera.com>
Date: Thu Sep 12 15:13:55 2013 -0700
mm: make lru_add_drain_all() selective
make lru_add_drain_all() only selectively interrupt the cpus that have
per-cpu free pages that can be drained.
This is important in nohz mode where calling mlockall(), for example,
otherwise will interrupt every core unnecessarily.
This is important on workloads where nohz cores are handling 10 Gb traffic
in userspace. Those CPUs do not enter the kernel and place pages into LRU
pagevecs and they really, really don't want to be interrupted, or they
drop packets on the floor.
Signed-off-by: Chris Metcalf <cmetcalf@tilera.com>
Reviewed-by: Tejun Heo <tj@kernel.org>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
diff --git a/include/linux/swap.h b/include/linux/swap.h
index c03c139219c9..46ba0c6c219f 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -280,7 +280,7 @@ extern void activate_page(struct page *);
extern void mark_page_accessed(struct page *);
extern void lru_add_drain(void);
extern void lru_add_drain_cpu(int cpu);
-extern int lru_add_drain_all(void);
+extern void lru_add_drain_all(void);
extern void rotate_reclaimable_page(struct page *page);
extern void deactivate_page(struct page *page);
extern void swap_setup(void);
diff --git a/mm/swap.c b/mm/swap.c
index c899502d3e36..759c3caf44bd 100644
--- a/mm/swap.c
+++ b/mm/swap.c
@@ -432,6 +432,11 @@ static void activate_page_drain(int cpu)
pagevec_lru_move_fn(pvec, __activate_page, NULL);
}
+static bool need_activate_page_drain(int cpu)
+{
+ return pagevec_count(&per_cpu(activate_page_pvecs, cpu)) != 0;
+}
+
void activate_page(struct page *page)
{
if (PageLRU(page) && !PageActive(page) && !PageUnevictable(page)) {
@@ -449,6 +454,11 @@ static inline void activate_page_drain(int cpu)
{
}
+static bool need_activate_page_drain(int cpu)
+{
+ return false;
+}
+
void activate_page(struct page *page)
{
struct zone *zone = page_zone(page);
@@ -701,12 +711,36 @@ static void lru_add_drain_per_cpu(struct work_struct *dummy)
lru_add_drain();
}
-/*
- * Returns 0 for success
- */
-int lru_add_drain_all(void)
+static DEFINE_PER_CPU(struct work_struct, lru_add_drain_work);
+
+void lru_add_drain_all(void)
{
- return schedule_on_each_cpu(lru_add_drain_per_cpu);
+ static DEFINE_MUTEX(lock);
+ static struct cpumask has_work;
+ int cpu;
+
+ mutex_lock(&lock);
+ get_online_cpus();
+ cpumask_clear(&has_work);
+
+ for_each_online_cpu(cpu) {
+ struct work_struct *work = &per_cpu(lru_add_drain_work, cpu);
+
+ if (pagevec_count(&per_cpu(lru_add_pvec, cpu)) ||
+ pagevec_count(&per_cpu(lru_rotate_pvecs, cpu)) ||
+ pagevec_count(&per_cpu(lru_deactivate_pvecs, cpu)) ||
+ need_activate_page_drain(cpu)) {
+ INIT_WORK(work, lru_add_drain_per_cpu);
+ schedule_work_on(cpu, work);
+ cpumask_set_cpu(cpu, &has_work);
+ }
+ }
+
+ for_each_cpu(cpu, &has_work)
+ flush_work(&per_cpu(lru_add_drain_work, cpu));
+
+ put_online_cpus();
+ mutex_unlock(&lock);
}
/*
]
|
| 45/2 |
2026/06/09 12:36 |
llm |
expert |
0m
Model:
Tokens:
input: 37817
output: 33
thoughts: 3695
Thoughts: **Analyzing the Potential Data Race in `lru_add_drain_all()` and Its Implications**
Okay, so the commit message says `lru_add_drain_all()` selectively interrupts CPUs to drain their per-CPU free pages. It's an optimization, and missing a CPU *isn't* catastrophic in the immediate sense, but let's dive deeper. I'm concerned about cases like `invalidate_bdev` where we truly *need* all pages flushed. If `invalidate_bdev` misses pages because of this "selective interrupt", it could lead to issues, especially if the device is being unmounted or resized, potentially leaving dangling references. The fact that `ext4_put_super` calls `invalidate_bdev` adds more weight to this concern, even if `invalidate_bdev`'s return value isn't explicitly checked. Ultimately, `lru_add_drain_all` is often a key piece to guaranteeing the expected `folio_ref_count`, so any missed CPU could lead to unexpected ref counts.
Now, this race *should* be extremely narrow, but KCSAN flagged it as a simple read/write race, prompting a closer look.
I'm digging into the kernel source, specifically `mm/swap.c`. The `cpu_needs_drain` function caught my eye, with all those `folio_batch_count` calls which are plain reads. `folio_batch_add` is the writer, a plain RMW operation that increments `nr`. The `folio_batch_count` itself is a simple inline read of `fbatch->nr`. If this is a known race, it should be marked. What I see is that it is being marked in a similar way, with `data_race()`.
I'm thinking about the implications. Imagine `lru_add_drain_all()` called from `alloc_contig_range()`. If a page is in a per-CPU batch, it can't be migrated, so the function relies on the flushing. Missing that CPU's batch would lead to a failure in `alloc_contig_range()` and could lead to `-EBUSY`.
The problem is the *ordering*. If a CPU adds a page to its batch *just* before `lru_add_drain_all()` runs, the read *might* miss the update. There's no explicit synchronization between the writer (the CPU adding to the batch) and the reader within `lru_add_drain_all()` aside from the `smp_mb()` at the beginning of `__lru_add_drain_all()`. This memory barrier *only* orders the stores of the same CPU. It does not help with a race like this. The reader needs a load-acquire, and the writer needs a store-release.
The `lru_drain_gen` logic is interesting, though. The generation number and memory barrier here. They're trying to prevent multiple concurrent drain calls from doing redundant work. The generation logic guarantees that the current batch is included in a drain that is running, or will be run soon. This generation logic combined with the `smp_mb()` at the start means that if we are draining, we know that any drain that started after us will see our changes, and if the drainer's `smp_mb()` is after a second CPU's `smp_mb()` (as required by the generation logic).
It ensures that if our update is missed, *we* will become the drainer. This works by a store-buffer pattern, or Dekker's algorithm, in other words.
The issue is that KCSAN is complaining about the race *itself*. While the logic *is* sound, the simple read/write race on `fbatch->nr` is technically undefined behavior in C. Compilers might optimize, and a memory barrier is needed to ensure correct behavior. In the kernel, we usually use `READ_ONCE()`/`WRITE_ONCE()` or `data_race()` to tell the compiler that we know what's up.
Here, `fbatch->nr` is being read without `READ_ONCE()` and written without `WRITE_ONCE()`.
The worst case is:
If the compiler reorders the stores in `folio_batch_add`.
But `nr` is 1 byte, RMW on x86 is atomic in the sense that it doesn't tear, but is not a "locked" atomic. The RMW is safe from other writers (due to `local_lock`).
If the reader sees a torn value? No. If the reader sees a stale value? The generation logic handles it.
The race is **BENIGN** from a *logic* standpoint, but it should still be marked with `data_race
|
| 46/2 |
2026/06/09 12:37 |
tool |
read-file |
0m
Args: map[File:mm/swap.c FirstLine:781 LineCount:15]
Results: map[Contents: 781: static bool cpu_needs_drain(unsigned int cpu)
782: {
783: struct cpu_fbatches *fbatches = &per_cpu(cpu_fbatches, cpu);
784:
785: /* Check these in order of likelihood that they're not zero */
786: return folio_batch_count(&fbatches->lru_add) ||
787: folio_batch_count(&fbatches->lru_move_tail) ||
788: folio_batch_count(&fbatches->lru_deactivate_file) ||
789: folio_batch_count(&fbatches->lru_deactivate) ||
790: folio_batch_count(&fbatches->lru_lazyfree) ||
791: folio_batch_count(&fbatches->lru_activate) ||
792: need_mlock_drain(cpu) ||
793: has_bh_in_lru(cpu, NULL);
794: }
795:
]
|
| 47/2 |
2026/06/09 12:37 |
llm |
expert |
0m
Model:
Tokens:
input: 42858
output: 28
thoughts: 1937
Thoughts: **Analysis of a Potential Data Race in Folio Batching**
Okay, so KCSAN flagged something, and I need to dive into this. It looks like there's a potential data race with `folio_batch_add`. Specifically, the race is between updating `fbatch->nr` (in `deactivate_file_folio`) and reading it (in `cpu_needs_drain`).
My first thought is about the compiler reordering the increment of `nr` before the store to `folios[]`. That's fine, the drain will just happen. But what about more exotic compiler behavior, like storing a temporary value during the increment? Unlikely, especially for a single-byte increment.
I've traced the execution paths. It's `ext4_put_super` -> `invalidate_bdev` -> `invalidate_mapping_pages` -> `mapping_try_invalidate` -> `deactivate_file_folio` (Task A) versus `ext4_put_super` -> `invalidate_bdev` -> `lru_add_drain_all` -> `__lru_add_drain_all` -> `cpu_needs_drain` (Task B). It seems that both tasks can race on the same `bdev` within `ext4_put_super`, even though `cpu_fbatches` is per-CPU. `lru_add_drain_all` is designed to handle this, checking all CPUs.
I'm starting to think this is probably a benign race. It's a single-byte read/write, so no tearing. The logic is designed to be tolerant of stale values (delayed drain), and the drain will eventually happen.
However, KCSAN is right; it *is* a data race. Adding `data_race()` or `READ_ONCE()`/`WRITE_ONCE()` is the right thing to do. Given that `fbatch->nr` is used in many places, maybe `READ_ONCE()` in `folio_batch_count` is appropriate, since it's always safe and often preferred for shared variables.
I need to confirm there's no risk of the compiler screwing up the `fbatch->nr++`. Using `fbatch->nr` as a scratch space? Highly unlikely. What about overflow? Nope, `FOLIO_BATCH_SIZE` is 15, `nr` is `unsigned char`. No worries. Multiple threads? Nope, per-cpu and protected by `local_lock`.
I'm feeling pretty confident this is **BENIGN**.
Let me re-examine the drain logic one more time. CPU 1 is the drainer. If it misses CPU 0's update, CPU 0 is the one that just added the folio. Does CPU 0 call `lru_add_drain_all`? No. The stack traces confirm that. `invalidate_bdev` calls `lru_add_drain_all` first, and then it calls `invalidate_mapping_pages`. So, the tasks have already completed one drain, and then one task is now adding folios that were missed in the previous drain.
So, Task 3307 has already done its drain and now adds folios. Task 3306 will eventually call `invalidate_mapping_pages` and try to evict that same page.
Both tasks could be unmounting the same filesystem and competing to clean up the same block device. That's fine.
KCSAN says "read-write to ... of 1 bytes" which confirms it's the `fbatch->nr++`. The value changed from `0x00` to `0x01`. This is for the first element being added to the batch.
If this were harmful, we'd see some sort of failure. A delayed drain of a "deactivate" hint is not going to cause a failure. The logic is robust. The race is between `invalidate_mapping_pages` (best-effort invalidation) and `truncate_inode_pages`. If we don't fully invalidate the pages, they will get cleaned up when the block device is closed.
I'm going to stick with my conclusion: it's **BENIGN**.
Let me quickly check for other related issues. `percpu_pvec_drained`? Not involved. `fbatch->i`? No. The race is purely on the `nr` count.
The recommended annotations are `data_race()` or `READ_ONCE()`/`WRITE_ONCE()`, but the kernel is moving toward `data_race()`. Let me scan for other instances of this in `mm/swap.c`.
|
| 48/2 |
2026/06/09 12:37 |
tool |
grepper |
0m
Args: map[Expression:data_race PathPrefix:mm/swap.c]
Results: map[Output:mm/swap.c=648=void lru_add_drain_cpu(int cpu)
--
mm/swap.c-657- /* Disabling interrupts below acts as a compiler barrier. */
mm/swap.c:658: if (data_race(folio_batch_count(fbatch))) {
mm/swap.c-659- unsigned long flags;
]
|
| 49/2 |
2026/06/09 12:37 |
llm |
expert |
0m
Model:
Tokens:
input: 45929
output: 15
thoughts: 2313
Thoughts: **Analysis of Potential Data Race in Folio Batch Management**
Okay, so I'm looking at this data race report, and the first thing I notice is it points to line 658, and there's another one in `lru_add_drain_all`? I remember seeing something there before, but let me double-check `cpu_needs_drain` again. The proposed fix, based on the discussion, is to use `data_race()` around the calls in `cpu_needs_drain`. That makes sense.
Now, should I just accept `data_race()`? Some people do prefer `READ_ONCE()` for these situations. But the real question is whether this race is *actually* a problem, or if it's benign.
Here's the situation: Imagine a task (CPU 0, Task A) calls `deactivate_file_folio`, adding a folio to a batch. Concurrently, another task (CPU 1, Task B) calls `lru_add_drain_all`, which checks if the batch *needs* to be drained using `cpu_needs_drain`. The interleaving could result in Task B missing Task A's update.
`deactivate_file_folio` is just a hint, right? The generation logic in `lru_add` should handle it, But does that still hold true if CPU 1 is the *only* one calling `lru_add_drain_all`? If CPU 1 calls it, it increments `lru_drain_gen`. If it misses CPU 0's update, it finishes. CPU 0 never called `lru_add_drain_all`. The folio stays in CPU 0's batch.
Is this actually a problem? It's no worse than if Task A had just *slightly* delayed adding the folio. The folio is still in the batch. The primary purpose of `lru_add_drain_all()` in `invalidate_bdev()` is to flush what's *already* there. If there's concurrent activity, "what's there" is somewhat ill-defined anyway.
Could there be *guaranteed* visibility if Task A had finished before Task B started? Only with synchronization. If they're unmounting the same filesystem, they might be synchronized by a higher-level lock. `ext4_put_super` uses `s_umount`, but how can there be two tasks in `ext4_put_super` at the same time? Oh, it's called from `generic_shutdown_super`, which in turn gets called from `kill_block_super` or `deactivate_locked_super`, which *shouldn't* allow concurrent calls for the *same* `sb`.
So, the KCSAN report shows two different tasks doing `ext4_put_super`. Are they on *different* filesystems? (Loop devices on the same file? No, that's not possible). The log message says `EXT4-fs (loop4): unmounting filesystem ...`. Does it say it twice? But only shows one such message.
If different filesystems, they have different `sb` and `bdev`. But the race is on the *same* address. That's `&per_cpu(cpu_fbatches, 0).lru_deactivate_file.nr`. `cpu_fbatches` is a per-CPU *global* variable. So, Task A on CPU 0 is adding from one filesystem, while Task B on CPU 1 is draining folios from *multiple* filesystems. This makes perfect sense; `cpu_fbatches` is a global resource. The race *is* global and can happen between any tasks performing memory management.
Is `fbatch->nr` at risk of corruption? No, because only the local CPU writes to it. The remote CPU *only* reads it. So, the conclusion is: **BENIGN**.
One detail: KCSAN says "read-write ... by task 3307." `folio_batch_add` increments with `fbatch->nr++`. This *is* a read of the old value and then a write of the new value. KCSAN is correct in identifying it as read-write.
The recommended fix is to use `data_race()` or `READ_ONCE()` for the read in `cpu_needs_drain()`. Really, changing `folio_batch_count()` to `READ_ONCE()` covers all uses of this value. And the compiler knows about read-once so it is probably the most suitable solution. `fbatch->nr` is `unsigned char`. `READ_ONCE` on `unsigned char` is fine.
However
|
| 50/2 |
2026/06/09 12:38 |
tool |
set-results |
0m
Args:
Results:
|
| 51/2 |
2026/06/09 12:38 |
llm |
expert |
0m
Model:
Tokens:
input: 49295
output: 755
thoughts: 448
Thoughts: **My Analysis of the Folio Batch Race Condition**
Okay, I've finished digging into this. It looks like the core of the issue is a potential data race between `folio_batch_add` (the writer) and `cpu_needs_drain` (the reader) on the `nr` field within a `struct folio_batch`. `cpu_needs_drain` is part of `lru_add_drain_all`, which is essentially a global sweep, checking each CPU for folios that need to be moved to the LRU lists from per-CPU batches.
The good news is, I'm pretty sure this race is benign. Here's why:
1. **Atomicity is on our side:** Because `nr` is only an `unsigned char` (a single byte), accesses to it are inherently atomic across all supported architectures. There's no chance of torn reads, which is a big relief.
2. **It's a Heuristic:** `cpu_needs_drain()` is designed to be a fast, quick check, to see if a CPU has anything that needs attention. If the reader sees a stale value (e.g., 0 when it should be 1), it's just going to skip that CPU in *this* round. It's not the end of the world if it misses a folio one time; we're essentially accepting a slight delay in LRU entry as a trade off for efficiency.
3. **Synchronization Handles Correctness:** `__lru_add_drain_all()` itself employs a robust synchronization protocol. It uses a global generation counter (`lru_drain_gen`) and memory barriers (`smp_mb`). This mechanism guarantees that if a process *needs* to ensure its added folios are drained (for correctness), it'll either do it itself or wait for a concurrent drainer that *will* see the updated state, even in the face of this potential race.
4. **Mutual Exclusion During the Drain:** The actual draining process β performed by a work item on the target CPU β acquires the same `local_lock` that `folio_batch_add` uses. This means that when the drain actually *happens*, the drainer *will* see a consistent view of the `folio_batch`, because the writer isn't simultaneously modifying it.
5. **We've Seen it Before:** I can see that similar lockless checks of `folio_batch` counts are already annotated with `data_race()` in the kernel; for example, the check in `mm/swap.c:658`. This gives me added confidence that this is a known pattern.
My recommendation? To be extra careful, I'd suggest wrapping the reads in `cpu_needs_drain()` with `data_race()` or `READ_ONCE()`. Honestly, the cleanest approach might be to update `folio_batch_count()` to consistently use `READ_ONCE()`, so we have a single, clean point of access that is safe. This keeps it consistent and minimizes risk, even though I'm confident in the benign nature of the race.
|
| 52/1 |
2026/06/09 12:38 |
action |
format-explanation |
0m
Results: map[Explanation:The KCSAN data race report identifies a race between `deactivate_file_folio()`
(the writer) and `__lru_add_drain_all()` (the reader) on the `nr` field of a
per-CPU `struct folio_batch`.
### Race Summary
* **Task A (CPU 0):** Executing `deactivate_file_folio()`, which calls
`folio_batch_add()`. This function performs a read-modify-write (`fbatch->nr++`)
to increment the number of folios in the per-CPU batch.
* **Task B (CPU 1):** Executing `__lru_add_drain_all()`, which calls
`cpu_needs_drain()`. This function performs a lockless read of `fbatch->nr` (via
`folio_batch_count()`) on remote CPUs to decide whether to schedule a drain work
item.
### Synchronization Context
The writer holds a `local_lock` (which on non-RT kernels is typically
`preempt_disable()`), ensuring mutual exclusion against other writers on the
same CPU. However, the reader on a remote CPU accesses the field without any
locks.
### Final Classification: **BENIGN**
### Reasoning
The race is structurally tolerant to stale reads and does not lead to logic
errors or crashes:
1. **No Tearing:** The racing variable `fbatch->nr` is an `unsigned char` (1
byte). On all supported architectures, a 1-byte access is atomic; the reader
will see either the old value or the new value, but never a corrupted/torn
value.
2. **Heuristic Optimization:** `cpu_needs_drain()` is a heuristic used to avoid
the overhead of queuing work items on CPUs that have empty batches. If the
reader sees a stale value of `0` when a folio has just been added, it simply
skips the drain for that CPU in the current round. This is a "missed
optimization" rather than a logic bug.
3. **Generation-Based Correctness:** The `__lru_add_drain_all()` function uses
a global generation counter (`lru_drain_gen`) and memory barriers (`smp_mb()`)
to ensure correctness. If a task requires its folios to be drained (e.g., for
`invalidate_bdev()`), the protocol guarantees that either this task or a
subsequent drainer will see the updated `nr` and perform the drain.
4. **Safe Draining:** When a drain is actually performed, it is done by a work
item running on the target CPU which acquires the `local_lock`. This ensures
that the drainer sees the fully initialized `folios[]` array and a consistent
`nr` count.
5. **Precedent:** Similar lockless checks of batch counts in `mm/swap.c` are
already acknowledged as safe and sometimes marked with `data_race()` (e.g., in
`lru_add_drain_cpu()`).
### Recommended Annotations
To silence the KCSAN report and prevent potential (though unlikely) compiler
optimizations from breaking the logic, the reads in `cpu_needs_drain()` should
be wrapped in `data_race()` or `READ_ONCE()`. Alternatively,
`folio_batch_count()` could be updated to use `READ_ONCE()`:
```c
static inline unsigned int folio_batch_count(const struct folio_batch *fbatch)
{
return READ_ONCE(fbatch->nr);
}
```]
|