| 0/0 |
2026/06/02 15:19 |
flow |
assessment-security |
1h06m
Results: map[DenialOfService:false Explanation:Based on the analysis of the kernel source code and similar historical bugs,
here is the assessment of the bug:
**Bug Mechanism & Impact:**
The `BUG: Bad rss-counter state` warning is triggered in `check_mm()` during
process termination when the kernel detects that the Resident Set Size (RSS)
counters for the memory descriptor (`mm_struct`) do not drop to zero. In this
specific report, the counters indicate an imbalance of `1` for `MM_FILEPAGES`
and `32` for `MM_SWAPENTS` (which corresponds to exactly 128KB or 32 pages of
swap entries, likely related to a multi-size THP or a batched operation).
Historically, these bugs are almost exclusively pure accounting errors caused by
race conditions or mishandled corner cases in the memory management subsystem
(e.g., `userfaultfd` moving pages between VMAs, `uprobe` replacing pages without
properly updating counters, or `madvise` operations on huge pages).
Because the warning is emitted via `pr_alert()` and does not halt the kernel (no
`panic()` or `BUG()`), it does not directly cause a Denial of Service (DoS) in
production environments. Furthermore, since it is an accounting imbalance rather
than a memory corruption (like a use-after-free or out-of-bounds access), it is
not exploitable for privilege escalation or arbitrary code execution.
**Trigger Context:**
- **Unprivileged / User Namespace:** The bug is triggered by `syzbot` running
unprivileged fuzzing programs (frequently utilizing `userfaultfd`, `madvise`, or
`uprobe` which are accessible to unprivileged users, often within user
namespaces).
- **Remote / Network / Peripheral / Filesystem:** This is a local memory
management issue and cannot be triggered via network packets, malicious
filesystems, or peripheral devices.
- **VM Context:** It is a standard host kernel MM bug and is not related to VM
guest/host transitions or confidential computing contexts.
**Conclusion:**
The bug is a local, unprivileged memory accounting anomaly. It does not pose a
risk of memory corruption, privilege escalation, or system crashes in production
builds. Exploitable:false FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:true UserNamespace:true VMGuestTrigger:false VMHostTrigger:false]
|
| 1/1 |
2026/06/02 15:19 |
action |
syz-repro-to-c-repro |
0m
Results:
|
| 2/1 |
2026/06/02 15:19 |
action |
kernel-checkouter |
2m
Results: map[KernelSrc:/app/workdir/cache/src/68b632eedabb62ff839db69157bbf939d8cb1733]
|
| 3/1 |
2026/06/02 15:21 |
action |
kernel-builder |
28m
Results: map[KernelObj:/app/workdir/cache/build/708eae986edab47b3c9863a14b0ad3aaa35c249d]
|
| 4/1 |
2026/06/02 15:49 |
action |
codesearch-prepare |
22m
Results: map[Index:codesearch-index]
|
| 5/1 |
2026/06/02 16:12 |
agent |
expert |
13m
Model:
Results: map[DenialOfService:false Exploitable:false FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:true UserNamespace:true VMGuestTrigger:false VMHostTrigger:false]
Instruction: You are an experienced Linux kernel security engineer. Your task is to analyze given kernel bug report
and determine its security impact based on the following dimensions.
Use the provided tools to examine the source code, check for capability checks (e.g., capable(), ns_capable()),
and understand the nature of the bug. Analyze the given kernel build and configuration.
You can check the kernel config by grepping ".config" file; you can check kernel cmdline by grepping
".config" file for "CONFIG_CMDLINE=". Assume sysctl parameters have default values.
But analyze for the corresponding production build w/o debugging tools enabled (like KASAN, KMSAN, UBSAN).
Try different strategies when analyzing the bug:
- think of ways in which the vulnerable code is unreachable
- or the other way around: try to come up with different ideas of how an unprivileged user can reach the bug
If still unsure err on the side of the bug being non-exploitable/not-accessible.
In the final reply, provide a reasoning for your assessment.
Analysis dimensions:
* Exploitable:
Determine if the bug can result in memory corruption or elevated privileges.
Memory safety issues are almost always exploitable (KASAN or UBSAN reports for use-after-free, out-of-bounds;
refcounting issues, corrupted lists, etc). When kernel is crashing on a completely wild pointer access
(e.g. user-space address, or non-canonical address, but not on NULL or address corresponding to KASAN shadow
for NULL address), including both data accesses and control transfers, that also usually implies possibility
of exploitation. Such reports usually say "unable to handle kernel paging request".
Uses of uninitialized values detected by KMSAN may be exploitable b/c attacker frequently can affect uninit
values with spraying techniques. However, for these exploitability depends on how exactly the uninit value
is used in the code, and what it affects.
Think of what happens after the bug is triggered. Some bugs cause kernel panic and halt execution,
they are harder to exploit. For example, BUG reports halts the kernel. However, WARNING reports don't halt
execution in production builds. Debug bug detection tools (like KASAN, KMSAN, KCSAN, UBSAN) are also not enabled
in production builds, so attacker can freely exploit these bugs w/o being detected by these tools.
If you see an integer overflow, think how the overflowed value used later (if it's used as allocation size,
or an array index). If you see an out-of-bounds read, think if it's followed by an out-of-bounds write as well.
Some KCSAN data-races may be exploitable by skilled attackers as well. Think what data structures got corrupted
as the result of data races and how. However, note that kernel has lots of "benign" data races that don't lead
to any runtime misbehavior at all.
* Denial Of Service:
Determine if the bug can result in denial-of-service. Most bugs can, since they cause system crash,
hangs, deadlocks, or resource leaks. This is mostly applicable to WARNING bugs that won't cause system crash
in production. For these think what will be consequences of the violation of the kernel assumptions flagged
by the WARNING. In some cases the unexpected condition is also properly handled by the normal control flow
(e.g. with "if (WARN_ON(...))"), these won't cause denial-of-service. If the condition is not handled,
then it may or may not cause denial-of-service.
* Accessible From Unprivileged Processes:
Determine if the bug can be reached from a typical (non-root) user process that does NOT have any special capabilities
(like CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON) or access to device nodes restricted to root.
Assume that unprivileged_bpf_disabled=1, that is eBPF loading is not accessible. However, cBPF (classical BPF)
is still accessible to non-root processes.
Assume that user namespaces are not accessible, that is, the process cannot get the mentioned capabilities even
within a new user namespace (checked by ns_capable() function in the kernel sources).
* Accessible From User Namespaces:
Determine if the bug can be reached within a user-namespace where the process has all capabilities
(including CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON). Such capabilities are checked with ns_capable()
function in the kernel sources.
* VM Guest Trigger:
Determine if the bug can be triggered from the context of a typical KVM guest (e.g., set up by a QEMU VMM).
Consider accesses to standard Linux host paravirtualized features (virtio-blk, virtio-net, etc.),
and handling of VM exits in the KVM code.
* VM Host Trigger in The Confidential Computing Context:
Determine if the bug can be triggered in a confidential computing guest kernel from the context of a KVM host.
Consider access to standard Linux guest paravirtualized features (virtio-blk, virtio-net, etc.).
* Ethernet Network Trigger:
Determine if the bug can be triggered by processing ingress network Ethernet traffic, either directly (network stack)
or via drivers exposed to network data.
* Other Remote Trigger:
Determine if the bug can be triggered by processing remote traffic other than Ethernet (Wifi, Bluetooth, NFC, etc).
* Peripheral Trigger:
Determine if the bug can be triggered via an untrusted peripheral device that can be physically plugged
into a system, such as a USB device or a niche hardware driver handling external hardware inputs.
This is particularly important for mobile and desktop environments where users can plug in unknown devices.
* Malicious Filesystem Trigger:
Determine if the bug can be triggered by the kernel mounting and parsing a malicious filesystem image.
This is highly critical for Desktop and Mobile environments where external media or downloaded images
might be auto-mounted.
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 kernel bug report is:
BUG: Bad rss-counter state mm:ffff88809d61a4c0 type:MM_FILEPAGES val:1 Comm:syz.0.18470 Pid:4124
BUG: Bad rss-counter state mm:ffff88809d61a4c0 type:MM_SWAPENTS val:32 Comm:syz.0.18470 Pid:4124
Reply: Based on the analysis of the kernel source code and similar historical bugs, here is the assessment of the bug:
**Bug Mechanism & Impact:**
The `BUG: Bad rss-counter state` warning is triggered in `check_mm()` during process termination when the kernel detects that the Resident Set Size (RSS) counters for the memory descriptor (`mm_struct`) do not drop to zero. In this specific report, the counters indicate an imbalance of `1` for `MM_FILEPAGES` and `32` for `MM_SWAPENTS` (which corresponds to exactly 128KB or 32 pages of swap entries, likely related to a multi-size THP or a batched operation).
Historically, these bugs are almost exclusively pure accounting errors caused by race conditions or mishandled corner cases in the memory management subsystem (e.g., `userfaultfd` moving pages between VMAs, `uprobe` replacing pages without properly updating counters, or `madvise` operations on huge pages).
Because the warning is emitted via `pr_alert()` and does not halt the kernel (no `panic()` or `BUG()`), it does not directly cause a Denial of Service (DoS) in production environments. Furthermore, since it is an accounting imbalance rather than a memory corruption (like a use-after-free or out-of-bounds access), it is not exploitable for privilege escalation or arbitrary code execution.
**Trigger Context:**
- **Unprivileged / User Namespace:** The bug is triggered by `syzbot` running unprivileged fuzzing programs (frequently utilizing `userfaultfd`, `madvise`, or `uprobe` which are accessible to unprivileged users, often within user namespaces).
- **Remote / Network / Peripheral / Filesystem:** This is a local memory management issue and cannot be triggered via network packets, malicious filesystems, or peripheral devices.
- **VM Context:** It is a standard host kernel MM bug and is not related to VM guest/host transitions or confidential computing contexts.
**Conclusion:**
The bug is a local, unprivileged memory accounting anomaly. It does not pose a risk of memory corruption, privilege escalation, or system crashes in production builds.
|
| 6/2 |
2026/06/02 16:12 |
llm |
expert |
0m
Model:
Tokens:
input: 5551
output: 19
thoughts: 153
Thoughts: **Debugging a Memory Management Bug**
Okay, so I've got this bug report, and it looks like a nasty one: "Bad rss-counter state". Specifically, it's flagging RSS counters – `MM_FILEPAGES` and `MM_SWAPENTS` – as non-zero on `mm_struct` destruction. That's a classic memory management issue, likely a leak or a double-free situation. This is definitely going to require some digging. Let's see... the report gives us a memory address and the process ID, which is a good starting point for tracing this back. Time to dive into the kernel source and search for that error message. "Bad rss-counter state" is the keyword here; hopefully, that'll point me right to the relevant code. Need to figure out *why* these counters aren't getting cleared. This is going to be fun.
|
| 7/2 |
2026/06/02 16:12 |
tool |
grepper |
0m
Args: map[Expression:Bad rss-counter state]
Results: map[Output:kernel/fork.c=622=static void check_mm(struct mm_struct *mm)
--
kernel/fork.c-632- if (unlikely(x)) {
kernel/fork.c:633: pr_alert("BUG: Bad rss-counter state mm:%p type:%s val:%ld Comm:%s Pid:%d\n",
kernel/fork.c-634- mm, resident_page_types[i], x,
]
|
| 8/2 |
2026/06/02 16:12 |
llm |
expert |
0m
Model:
Tokens:
input: 6035
output: 32
thoughts: 30
Thoughts: Here's my analysis of the situation, presented from my perspective as an expert:
**Debugging the Memory Management Issue**
Okay, so the initial report strongly suggests the bug is nestled within the `check_mm` function, which is located in `kernel/fork.c`. My first instinct is to dive directly into that function. Let's get a handle on what `check_mm` actually *does*. I need to understand its intended purpose and the specific conditions under which it operates to see where the logic might be flawed, or where a race condition or incorrect assumption is potentially leading to the observed bug. My priority is to build a mental model of this function's control flow and data dependencies. Only then can I pinpoint the precise line or lines that are causing problems, or the conditions under which it fails.
|
| 9/2 |
2026/06/02 16:12 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/fork.c Name:check_mm]
Results: map[SourceCode: 622: static void check_mm(struct mm_struct *mm)
623: {
624: int i;
625:
626: BUILD_BUG_ON_MSG(ARRAY_SIZE(resident_page_types) != NR_MM_COUNTERS,
627: "Please make sure 'struct resident_page_types[]' is updated as well");
628:
629: for (i = 0; i < NR_MM_COUNTERS; i++) {
630: long x = percpu_counter_sum(&mm->rss_stat[i]);
631:
632: if (unlikely(x)) {
633: pr_alert("BUG: Bad rss-counter state mm:%p type:%s val:%ld Comm:%s Pid:%d\n",
634: mm, resident_page_types[i], x,
635: current->comm,
636: task_pid_nr(current));
637: }
638: }
639:
640: if (mm_pgtables_bytes(mm))
641: pr_alert("BUG: non-zero pgtables_bytes on freeing mm: %ld\n",
642: mm_pgtables_bytes(mm));
643:
644: #if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !defined(CONFIG_SPLIT_PMD_PTLOCKS)
645: VM_BUG_ON_MM(mm->pmd_huge_pte, mm);
646: #endif
647: }
SourceFile:kernel/fork.c]
|
| 10/2 |
2026/06/02 16:12 |
llm |
expert |
0m
Model:
Tokens:
input: 6733
output: 30
thoughts: 283
Thoughts: **Analyzing a Troubling RSS Counter Imbalance**
Okay, so I'm looking at this warning that pops up when an `mm_struct` is destroyed, and the RSS counters aren't zeroed out. The report itself is flagging a "Bad rss-counter state," specifically for `MM_FILEPAGES` and `MM_SWAPENTS`. The system's reporting an imbalance – values of 1 and 32 respectively. My first thought is, "Is this exploitable?" It's a `pr_alert` message, not a `BUG()` or a kernel panic, which is a good sign, meaning it's likely not immediately catastrophic. However, incorrect RSS counters always raise a red flag.
The implications are potentially serious: leaked pages, double-frees, or just plain incorrect accounting. If it's *just* an accounting error, it's probably benign, but if pages are being leaked, that's a DoS risk from eventually hitting OOM. A double-free situation, however, could be a serious vulnerability.
My experience tells me this usually boils down to pages being left mapped or incorrectly accounted for. I'm going to start digging into the git log. The next step is a search for this particular bug string to see if any context or previous fixes are available.
|
| 11/2 |
2026/06/02 16:12 |
tool |
git-log |
0m
Args: map[MessageRegexps:[Bad rss-counter state MM_SWAPENTS]]
Results:
|
| 12/2 |
2026/06/02 16:13 |
llm |
expert |
0m
Model:
Tokens:
input: 7335
output: 22
thoughts: 20
Thoughts: Okay, here's my interpretation of those thoughts, summarized from my perspective as an expert:
**Internal Troubleshooting Checklist**
Alright, time to get to the core of this. Forget the fluff, let's cut right to the chase. When I'm seeing performance anomalies, I immediately gravitate towards the most likely culprits. Instead of a laundry list of possibilities, I need the critical indicators. So, I'm thinking, "Bad rss-counter state" and "MM_SWAPENTS". Let's focus on these and see if they reveal the root cause. This is the starting point, the quick triage. It’s about leveraging my experience to zoom in on the most promising leads. These two data points, in my experience, can tell the entire story with just enough information.
|
| 13/2 |
2026/06/02 16:13 |
tool |
git-log |
0m
Args: map[MessageRegexps:[Bad rss-counter state]]
Results: map[Output:dde30854bddf mm/debug_vm_pgtable: clear page table entries at destroy_args()
881388f34338 mm: add process info to bad rss-counter warning
0ea148a79919 mm: userfaultfd: fix race of userfaultfd_move and swap cache
2b12d06c37fd mm: fix uprobe pte be overwritten when expanding vma
bddf10d26e6e uprobes: Reject the shared zeropage in uprobe_write_opcode()
a98460494b16 mm/khugepaged: fix collapse_pte_mapped_thp() versus uffd
e720e7d0e983 mm: fix race by making init_zero_pfn() early_initcall
542db12a9c42 powerpc: Fix random segfault when freeing hugetlb range
c445c1668dca drm/ttm: Temporarily disable the huge_fault() callback
8b272b3cbbb5 mm, numa: fix bad pmd by atomically check for pmd_trans_huge when marking page tables prot_numa
]
|
| 14/2 |
2026/06/02 16:13 |
llm |
expert |
0m
Model:
Tokens:
input: 7838
output: 26
thoughts: 34
Thoughts: Okay, let's dive into this patch: **`0ea148a79919 mm: userfaultfd: fix race of userfaultfd_move and swap cache`**.
Right, so this is focusing on a potential race condition within the userfaultfd subsystem, specifically between `userfaultfd_move` and the swap cache. I know userfaultfd well, and I've seen these kinds of memory management issues before. My initial thought is, "Ah, another subtle concurrency bug, likely tied to page migration or eviction." The "fix" implies there was a problem – a potential corruption, incorrect behavior, or maybe even a security vulnerability arising from a data race.
I'm immediately trying to visualize how these two functions interact. `userfaultfd_move` is likely involved in moving user-space pages around, perhaps during a fork or page migration requested from user-space. The swap cache, on the other hand, deals with the eviction and retrieval of pages to and from swap space. The intersection of these two areas is where the race probably lurks. The swap cache is a critical component, if there is a race condition affecting the swap cache it could be a major problem.
I'm thinking about the likely steps involved: a page is accessed, then potentially swapped out, but the `userfaultfd_move` may try to operate on the page at the same time and so we can run into issues with the swap cache. It's probably something like a page being added to the swap cache and then immediately modified or unmapped, leading to inconsistent state. The fix, then, is likely to introduce locking, ordering, or some form of synchronization to ensure that these operations don't collide.
I'll need to look at the patch itself to see the precise details, of course. But this is the kind of initial mental model I build when encountering a patch description like this. The keywords immediately point to a critical area of memory management, and potential pitfalls for a system's stability.
|
| 15/2 |
2026/06/02 16:13 |
tool |
git-show |
0m
Args:
Results: map[Output:commit 0ea148a799198518d8ebab63ddd0bb6114a103bc
Author: Kairui Song <kasong@tencent.com>
Date: Wed Jun 4 23:10:38 2025 +0800
mm: userfaultfd: fix race of userfaultfd_move and swap cache
This commit fixes two kinds of races, they may have different results:
Barry reported a BUG_ON in commit c50f8e6053b0, we may see the same
BUG_ON if the filemap lookup returned NULL and folio is added to swap
cache after that.
If another kind of race is triggered (folio changed after lookup) we
may see RSS counter is corrupted:
[ 406.893936] BUG: Bad rss-counter state mm:ffff0000c5a9ddc0
type:MM_ANONPAGES val:-1
[ 406.894071] BUG: Bad rss-counter state mm:ffff0000c5a9ddc0
type:MM_SHMEMPAGES val:1
Because the folio is being accounted to the wrong VMA.
I'm not sure if there will be any data corruption though, seems no.
The issues above are critical already.
On seeing a swap entry PTE, userfaultfd_move does a lockless swap cache
lookup, and tries to move the found folio to the faulting vma. Currently,
it relies on checking the PTE value to ensure that the moved folio still
belongs to the src swap entry and that no new folio has been added to the
swap cache, which turns out to be unreliable.
While working and reviewing the swap table series with Barry, following
existing races are observed and reproduced [1]:
In the example below, move_pages_pte is moving src_pte to dst_pte, where
src_pte is a swap entry PTE holding swap entry S1, and S1 is not in the
swap cache:
CPU1 CPU2
userfaultfd_move
move_pages_pte()
entry = pte_to_swp_entry(orig_src_pte);
// Here it got entry = S1
... < interrupted> ...
<swapin src_pte, alloc and use folio A>
// folio A is a new allocated folio
// and get installed into src_pte
<frees swap entry S1>
// src_pte now points to folio A, S1
// has swap count == 0, it can be freed
// by folio_swap_swap or swap
// allocator's reclaim.
<try to swap out another folio B>
// folio B is a folio in another VMA.
<put folio B to swap cache using S1 >
// S1 is freed, folio B can use it
// for swap out with no problem.
...
folio = filemap_get_folio(S1)
// Got folio B here !!!
... < interrupted again> ...
<swapin folio B and free S1>
// Now S1 is free to be used again.
<swapout src_pte & folio A using S1>
// Now src_pte is a swap entry PTE
// holding S1 again.
folio_trylock(folio)
move_swap_pte
double_pt_lock
is_pte_pages_stable
// Check passed because src_pte == S1
folio_move_anon_rmap(...)
// Moved invalid folio B here !!!
The race window is very short and requires multiple collisions of multiple
rare events, so it's very unlikely to happen, but with a deliberately
constructed reproducer and increased time window, it can be reproduced
easily.
This can be fixed by checking if the folio returned by filemap is the
valid swap cache folio after acquiring the folio lock.
Another similar race is possible: filemap_get_folio may return NULL, but
folio (A) could be swapped in and then swapped out again using the same
swap entry after the lookup. In such a case, folio (A) may remain in the
swap cache, so it must be moved too:
CPU1 CPU2
userfaultfd_move
move_pages_pte()
entry = pte_to_swp_entry(orig_src_pte);
// Here it got entry = S1, and S1 is not in swap cache
folio = filemap_get_folio(S1)
// Got NULL
... < interrupted again> ...
<swapin folio A and free S1>
<swapout folio A re-using S1>
move_swap_pte
double_pt_lock
is_pte_pages_stable
// Check passed because src_pte == S1
folio_move_anon_rmap(...)
// folio A is ignored !!!
Fix this by checking the swap cache again after acquiring the src_pte
lock. And to avoid the filemap overhead, we check swap_map directly [2].
The SWP_SYNCHRONOUS_IO path does make the problem more complex, but so far
we don't need to worry about that, since folios can only be exposed to the
swap cache in the swap out path, and this is covered in this patch by
checking the swap cache again after acquiring the src_pte lock.
Testing with a simple C program that allocates and moves several GB of
memory did not show any observable performance change.
Link: https://lkml.kernel.org/r/20250604151038.21968-1-ryncsn@gmail.com
Fixes: adef440691ba ("userfaultfd: UFFDIO_MOVE uABI")
Signed-off-by: Kairui Song <kasong@tencent.com>
Closes: https://lore.kernel.org/linux-mm/CAMgjq7B1K=6OOrK2OUZ0-tqCzi+EJt+2_K97TPGoSt=9+JwP7Q@mail.gmail.com/ [1]
Link: https://lore.kernel.org/all/CAGsJ_4yJhJBo16XhiC-nUzSheyX-V3-nFE+tAi=8Y560K8eT=A@mail.gmail.com/ [2]
Reviewed-by: Lokesh Gidra <lokeshgidra@google.com>
Acked-by: Peter Xu <peterx@redhat.com>
Reviewed-by: Suren Baghdasaryan <surenb@google.com>
Reviewed-by: Barry Song <baohua@kernel.org>
Reviewed-by: Chris Li <chrisl@kernel.org>
Cc: Andrea Arcangeli <aarcange@redhat.com>
Cc: David Hildenbrand <david@redhat.com>
Cc: Kairui Song <kasong@tencent.com>
Cc: <stable@vger.kernel.org>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
diff --git a/mm/userfaultfd.c b/mm/userfaultfd.c
index bc473ad21202..8253978ee0fb 100644
--- a/mm/userfaultfd.c
+++ b/mm/userfaultfd.c
@@ -1084,8 +1084,18 @@ static int move_swap_pte(struct mm_struct *mm, struct vm_area_struct *dst_vma,
pte_t orig_dst_pte, pte_t orig_src_pte,
pmd_t *dst_pmd, pmd_t dst_pmdval,
spinlock_t *dst_ptl, spinlock_t *src_ptl,
- struct folio *src_folio)
+ struct folio *src_folio,
+ struct swap_info_struct *si, swp_entry_t entry)
{
+ /*
+ * Check if the folio still belongs to the target swap entry after
+ * acquiring the lock. Folio can be freed in the swap cache while
+ * not locked.
+ */
+ if (src_folio && unlikely(!folio_test_swapcache(src_folio) ||
+ entry.val != src_folio->swap.val))
+ return -EAGAIN;
+
double_pt_lock(dst_ptl, src_ptl);
if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
@@ -1102,6 +1112,25 @@ static int move_swap_pte(struct mm_struct *mm, struct vm_area_struct *dst_vma,
if (src_folio) {
folio_move_anon_rmap(src_folio, dst_vma);
src_folio->index = linear_page_index(dst_vma, dst_addr);
+ } else {
+ /*
+ * Check if the swap entry is cached after acquiring the src_pte
+ * lock. Otherwise, we might miss a newly loaded swap cache folio.
+ *
+ * Check swap_map directly to minimize overhead, READ_ONCE is sufficient.
+ * We are trying to catch newly added swap cache, the only possible case is
+ * when a folio is swapped in and out again staying in swap cache, using the
+ * same entry before the PTE check above. The PTL is acquired and released
+ * twice, each time after updating the swap_map's flag. So holding
+ * the PTL here ensures we see the updated value. False positive is possible,
+ * e.g. SWP_SYNCHRONOUS_IO swapin may set the flag without touching the
+ * cache, or during the tiny synchronization window between swap cache and
+ * swap_map, but it will be gone very quickly, worst result is retry jitters.
+ */
+ if (READ_ONCE(si->swap_map[swp_offset(entry)]) & SWAP_HAS_CACHE) {
+ double_pt_unlock(dst_ptl, src_ptl);
+ return -EAGAIN;
+ }
}
orig_src_pte = ptep_get_and_clear(mm, src_addr, src_pte);
@@ -1412,7 +1441,7 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
}
err = move_swap_pte(mm, dst_vma, dst_addr, src_addr, dst_pte, src_pte,
orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
- dst_ptl, src_ptl, src_folio);
+ dst_ptl, src_ptl, src_folio, si, entry);
}
out:
]
|
| 16/2 |
2026/06/02 16:13 |
llm |
expert |
0m
Model:
Tokens:
input: 11090
output: 38
thoughts: 427
Thoughts: **Analyzing a Potential Kernel Memory Accounting Bug**
Okay, so I'm looking at this commit message. It seems like Barry reported a `BUG_ON` related to a race condition. It seems to be triggered when a filemap lookup returns NULL and the folio is added to the swap cache afterward. The message also says there's a risk of RSS counters getting corrupted if the folio changes after the lookup, which could lead to incorrect VMA accounting. It mentions potential corruption of `MM_ANONPAGES` and `MM_SHMEMPAGES`, but says data corruption is unlikely, although the RSS counter issue is already critical.
But wait, my bug report! I have a similar-looking `BUG: Bad rss-counter state` report, but mine involves `MM_FILEPAGES` and `MM_SWAPENTS` counters. And the mm is the same. The process information matches. The key difference seems to be the counter types. I also note that the commit is dated June 4, 2025. That has to be an error! Okay, let's disregard that and check the context. It does deal with the `userfaultfd_move` and swap cache.
This seems like the key connection. This could be exactly the same bug with different counters. I need to figure out if my bug report is related to userfaultfd. I need to search syzbot and the git logs to see if anyone else has reported something similar. Understanding this connection is critical to resolving the memory accounting issue.
|
| 17/2 |
2026/06/02 16:13 |
tool |
git-log |
0m
Args: map[MessageRegexps:[Bad rss-counter state MM_FILEPAGES MM_SWAPENTS]]
Results:
|
| 18/2 |
2026/06/02 16:13 |
llm |
expert |
0m
Model:
Tokens:
input: 11888
output: 30
thoughts: 20
Thoughts: Okay, here's my summarized thought process, geared towards a user who, like myself, has a strong technical understanding:
**Investigating a Potentially Corrupted RSS Counter and Memory Swap Issue**
Alright, time to dive in. My initial hunch is pointing towards a problem related to the RSS counter, potentially in a "bad state," combined with something involving "MM_SWAPENTS." This pairing of terms screams memory management issues to me. Likely, a corruption or inaccurate reporting of RSS (Resident Set Size) is at play, and that's interacting somehow with the memory swap mechanism. My gut says this will likely be a good starting point for finding my way to the solution. Perhaps a memory leak, corruption, or improper handling of memory segments. Let's see what the logs will tell us.
|
| 19/2 |
2026/06/02 16:13 |
tool |
git-log |
0m
Args: map[MessageRegexps:[Bad rss-counter state MM_SWAPENTS]]
Results:
|
| 20/2 |
2026/06/02 16:14 |
llm |
expert |
0m
Model:
Tokens:
input: 12118
output: 30
thoughts: 20
Thoughts: Here's my thought process, summarized as you requested:
**Investigating a Potential Issue**
Okay, time to dig in. I need to figure out what's going on. Let's start with a focused search. My initial hunch is that we're dealing with a "Bad rss-counter state" issue. This points toward potential memory management problems or corruption related to RSS, which is a known area of complexity. I'll need to correlate this with other factors. My next step will be to search for anything related to "MM_FILEPAGES." This is a key memory management structure in many systems, and if there's a problem with the counter state, the FilePages counter is likely to be involved. By combining these two search terms, I should be able to narrow down the scope and hopefully find relevant logs, error messages, or even related bug reports. This targeted approach is the best way to avoid being overwhelmed by unrelated information and to efficiently identify the root cause. I'm hoping this dual search will provide enough context to understand the issue.
|
| 21/2 |
2026/06/02 16:14 |
tool |
git-log |
0m
Args: map[MessageRegexps:[Bad rss-counter state MM_FILEPAGES]]
Results: map[Output:bddf10d26e6e uprobes: Reject the shared zeropage in uprobe_write_opcode()
542db12a9c42 powerpc: Fix random segfault when freeing hugetlb range
8495f7e6732e fork: improve error message for corrupted page tables
]
|
| 22/2 |
2026/06/02 16:14 |
llm |
expert |
0m
Model:
Tokens:
input: 12477
output: 25
thoughts: 33
Thoughts: Here's my take on those thoughts:
**Analyzing the Uprobes Patch: `bddf10d26e6e` and Zeropage Concerns**
Okay, so I'm looking at commit `bddf10d26e6e` specifically, dealing with uprobes. The title, "Reject the shared zeropage in uprobe_write_opcode()", immediately flags a potential memory protection issue. My expert instincts are telling me this is likely related to preventing unauthorized modification of the shared zero page when writing probe opcodes. I'm imagining scenarios involving concurrent access to memory regions and the potential for a rogue uprobe to overwrite critical kernel data. Given that uprobes directly manipulate instruction streams, the shared zeropage is a known weakness. This patch probably addresses the security risk of an uprobe corrupting the system by writing to a location it shouldn't. I'm betting we're talking about preventing a privilege escalation vulnerability here. I need to dig into the code and see how this specific commit implements that rejection. I need to understand the exact conditions under which the shared zeropage is being avoided to be certain.
|
| 23/2 |
2026/06/02 16:14 |
tool |
git-show |
0m
Args:
Results: map[Output:commit bddf10d26e6e5114e7415a0e442ec6f51a559468
Author: Tong Tiangen <tongtiangen@huawei.com>
Date: Mon Feb 24 11:11:49 2025 +0800
uprobes: Reject the shared zeropage in uprobe_write_opcode()
We triggered the following crash in syzkaller tests:
BUG: Bad page state in process syz.7.38 pfn:1eff3
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x1eff3
flags: 0x3fffff00004004(referenced|reserved|node=0|zone=1|lastcpupid=0x1fffff)
raw: 003fffff00004004 ffffe6c6c07bfcc8 ffffe6c6c07bfcc8 0000000000000000
raw: 0000000000000000 0000000000000000 00000000fffffffe 0000000000000000
page dumped because: PAGE_FLAGS_CHECK_AT_FREE flag(s) set
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1.1 04/01/2014
Call Trace:
<TASK>
dump_stack_lvl+0x32/0x50
bad_page+0x69/0xf0
free_unref_page_prepare+0x401/0x500
free_unref_page+0x6d/0x1b0
uprobe_write_opcode+0x460/0x8e0
install_breakpoint.part.0+0x51/0x80
register_for_each_vma+0x1d9/0x2b0
__uprobe_register+0x245/0x300
bpf_uprobe_multi_link_attach+0x29b/0x4f0
link_create+0x1e2/0x280
__sys_bpf+0x75f/0xac0
__x64_sys_bpf+0x1a/0x30
do_syscall_64+0x56/0x100
entry_SYSCALL_64_after_hwframe+0x78/0xe2
BUG: Bad rss-counter state mm:00000000452453e0 type:MM_FILEPAGES val:-1
The following syzkaller test case can be used to reproduce:
r2 = creat(&(0x7f0000000000)='./file0\x00', 0x8)
write$nbd(r2, &(0x7f0000000580)=ANY=[], 0x10)
r4 = openat(0xffffffffffffff9c, &(0x7f0000000040)='./file0\x00', 0x42, 0x0)
mmap$IORING_OFF_SQ_RING(&(0x7f0000ffd000/0x3000)=nil, 0x3000, 0x0, 0x12, r4, 0x0)
r5 = userfaultfd(0x80801)
ioctl$UFFDIO_API(r5, 0xc018aa3f, &(0x7f0000000040)={0xaa, 0x20})
r6 = userfaultfd(0x80801)
ioctl$UFFDIO_API(r6, 0xc018aa3f, &(0x7f0000000140))
ioctl$UFFDIO_REGISTER(r6, 0xc020aa00, &(0x7f0000000100)={{&(0x7f0000ffc000/0x4000)=nil, 0x4000}, 0x2})
ioctl$UFFDIO_ZEROPAGE(r5, 0xc020aa04, &(0x7f0000000000)={{&(0x7f0000ffd000/0x1000)=nil, 0x1000}})
r7 = bpf$PROG_LOAD(0x5, &(0x7f0000000140)={0x2, 0x3, &(0x7f0000000200)=ANY=[@ANYBLOB="1800000000120000000000000000000095"], &(0x7f0000000000)='GPL\x00', 0x7, 0x0, 0x0, 0x0, 0x0, '\x00', 0x0, @fallback=0x30, 0xffffffffffffffff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, @void, @value}, 0x94)
bpf$BPF_LINK_CREATE_XDP(0x1c, &(0x7f0000000040)={r7, 0x0, 0x30, 0x1e, @val=@uprobe_multi={&(0x7f0000000080)='./file0\x00', &(0x7f0000000100)=[0x2], 0x0, 0x0, 0x1}}, 0x40)
The cause is that zero pfn is set to the PTE without increasing the RSS
count in mfill_atomic_pte_zeropage() and the refcount of zero folio does
not increase accordingly. Then, the operation on the same pfn is performed
in uprobe_write_opcode()->__replace_page() to unconditional decrease the
RSS count and old_folio's refcount.
Therefore, two bugs are introduced:
1. The RSS count is incorrect, when process exit, the check_mm() report
error "Bad rss-count".
2. The reserved folio (zero folio) is freed when folio->refcount is zero,
then free_pages_prepare->free_page_is_bad() report error
"Bad page state".
There is more, the following warning could also theoretically be triggered:
__replace_page()
-> ...
-> folio_remove_rmap_pte()
-> VM_WARN_ON_FOLIO(is_zero_folio(folio), folio)
Considering that uprobe hit on the zero folio is a very rare case, just
reject zero old folio immediately after get_user_page_vma_remote().
[ mingo: Cleaned up the changelog ]
Fixes: 7396fa818d62 ("uprobes/core: Make background page replacement logic account for rss_stat counters")
Fixes: 2b1444983508 ("uprobes, mm, x86: Add the ability to install and remove uprobes breakpoints")
Signed-off-by: Tong Tiangen <tongtiangen@huawei.com>
Signed-off-by: Ingo Molnar <mingo@kernel.org>
Reviewed-by: David Hildenbrand <david@redhat.com>
Reviewed-by: Oleg Nesterov <oleg@redhat.com>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Link: https://lore.kernel.org/r/20250224031149.1598949-1-tongtiangen@huawei.com
diff --git a/kernel/events/uprobes.c b/kernel/events/uprobes.c
index bf2a87a0a378..af53fbd2d12c 100644
--- a/kernel/events/uprobes.c
+++ b/kernel/events/uprobes.c
@@ -495,6 +495,11 @@ int uprobe_write_opcode(struct arch_uprobe *auprobe, struct mm_struct *mm,
if (ret <= 0)
goto put_old;
+ if (is_zero_page(old_page)) {
+ ret = -EINVAL;
+ goto put_old;
+ }
+
if (WARN(!is_register && PageCompound(old_page),
"uprobe unregister should never work on compound page\n")) {
ret = -EINVAL;
]
|
| 24/2 |
2026/06/02 16:14 |
llm |
expert |
0m
Model:
Tokens:
input: 15159
output: 30
thoughts: 301
Thoughts: **Analyzing a BPF RSS Counter Bug**
Okay, so this commit addresses a problem with `uprobe_write_opcode` operating on a zero page. Apparently, this leads to incorrect RSS counts, and the zero page gets freed. The reproducer seems to be using `bpf$BPF_LINK_CREATE_XDP` with `uprobe_multi`. The specific bug report I'm looking at shows some bad RSS counter states: `MM_FILEPAGES val:1` and `MM_SWAPENTS val:32`.
Hold on... a previous commit, `bddf10d26e6e`, had a negative value for `MM_FILEPAGES` (val:-1), while mine is positive. That's interesting. I should probably re-examine the `MM_SWAPENTS` part as well, in relation to that previous commit, as that's unique to my report.
Perhaps searching for "Bad rss-counter state" and "MM_SWAPENTS" in syzbot issues again would give some insight. Or, could I look up this specific bug report string in the kernel tree? Oh right, those aren't directly in the tree. No matter, let's explore if there are any related commits that were introduced after that one.
|
| 25/2 |
2026/06/02 16:14 |
tool |
git-log |
0m
Args: map[MessageRegexps:[Bad rss-counter state MM_SWAPENTS]]
Results:
|
| 26/2 |
2026/06/02 16:15 |
llm |
expert |
0m
Model:
Tokens:
input: 15778
output: 25
thoughts: 91
Thoughts: **Investigating a Potential Userfaultfd-Related Memory Issue**
Okay, let's revisit commit `0ea148a79919`, the one fixing a race condition between `userfaultfd_move` and the swap cache. Hmm, the associated bug report mentions `MM_SWAPENTS val:32`. That rings a bell, I wonder if this issue is actually connected to `userfaultfd`? To get a clearer picture, I should probably search within the `mm/` directory for any modifications or uses of `MM_SWAPENTS`. That should help me identify where and how that particular variable is being manipulated, which might offer a crucial clue as to what's going on with this fault. Time to dive in.
|
| 27/2 |
2026/06/02 16:15 |
tool |
grepper |
0m
Args: map[Expression:inc_mm_counter.*MM_SWAPENTS]
Results: map[Output:mm/rmap.c=1964=static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
--
mm/rmap.c-2291- dec_mm_counter(mm, MM_ANONPAGES);
mm/rmap.c:2292: inc_mm_counter(mm, MM_SWAPENTS);
mm/rmap.c-2293- swp_pte = swp_entry_to_pte(entry);
]
|
| 28/2 |
2026/06/02 16:15 |
llm |
expert |
0m
Model:
Tokens:
input: 16189
output: 25
thoughts: 18
Thoughts: Here's a summary of my thought process, as if I were the one thinking it:
**Digging into Memory Management Counters**
Okay, so I'm looking to understand something about memory management counters, specifically how they relate to the `MM_SWAPENTS` metric. This tells me I need to start with some code-level exploration, so I'm thinking I should start by searching the codebase for the string `add_mm_counter.*MM_SWAPENTS`. The `add_mm_counter` part is pretty standard for adding a memory management counter, so that's my entry point. I'm hoping to find where and how `MM_SWAPENTS` is being incremented. This will show me the specific conditions under which swap entries are being tracked, the context in which this happens, and then I can deduce its significance. I'm expecting this search to get me a specific function and the calling context. Once I understand that, I can hopefully derive its implication within this specific memory management subsystem.
|
| 29/2 |
2026/06/02 16:15 |
tool |
grepper |
0m
Args: map[Expression:add_mm_counter.*MM_SWAPENTS]
Results: map[Output:mm/madvise.c=651=static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
--
mm/madvise.c-773- if (nr_swap)
mm/madvise.c:774: add_mm_counter(mm, MM_SWAPENTS, nr_swap);
mm/madvise.c-775- if (start_pte) {
--
mm/memory.c=4706=vm_fault_t do_swap_page(struct vm_fault *vmf)
--
mm/memory.c-5001- add_mm_counter(vma->vm_mm, MM_ANONPAGES, nr_pages);
mm/memory.c:5002: add_mm_counter(vma->vm_mm, MM_SWAPENTS, -nr_pages);
mm/memory.c-5003- pte = mk_pte(page, vma->vm_page_prot);
]
|
| 30/2 |
2026/06/02 16:15 |
llm |
expert |
0m
Model:
Tokens:
input: 16709
output: 25
thoughts: 18
Thoughts: Here's a summary of my thought process, as if I were the one thinking it:
**Digging into Memory Management**
Okay, so I need to track down something related to memory management and it looks like I'm dealing with counter mechanisms. The phrase `dec_mm_counter` jumps out at me immediately, signaling a decrementing memory counter, likely associated with some resource allocation or deallocation. The `.*` acts as a wildcard, which is crucial because I'm not entirely sure where this counter might be defined or used, or even the exact naming convention. This is good, I can widen my search.
The `MM_SWAPENTS` part is the key. That definitely points to the swap entry structures. Now, I'm thinking, this might be a counter that reflects the number of swap entries, or perhaps something related to the activity of these entries - their creation, deletion, or perhaps their active usage within the swap space. I suspect this counter is part of the system's larger memory management accounting. It could be crucial for performance tuning or pinpointing memory leaks, or perhaps an error. By searching for that entire string I should find it easily. Let's see what the system turns up!
|
| 31/2 |
2026/06/02 16:15 |
tool |
grepper |
0m
Args: map[Expression:dec_mm_counter.*MM_SWAPENTS]
Results: map[Output:arch/s390/mm/gmap_helpers.c=28=static void ptep_zap_softleaf_entry(struct mm_struct *mm, softleaf_t entry)
--
arch/s390/mm/gmap_helpers.c-30- if (softleaf_is_swap(entry))
arch/s390/mm/gmap_helpers.c:31: dec_mm_counter(mm, MM_SWAPENTS);
arch/s390/mm/gmap_helpers.c-32- else if (softleaf_is_migration(entry))
--
mm/swapfile.c=2080=static int unuse_pte(struct vm_area_struct *vma, pmd_t *pmd,
--
mm/swapfile.c-2121-
mm/swapfile.c:2122: dec_mm_counter(vma->vm_mm, MM_SWAPENTS);
mm/swapfile.c-2123- if (hwpoisoned) {
--
mm/swapfile.c-2139-
mm/swapfile.c:2140: dec_mm_counter(vma->vm_mm, MM_SWAPENTS);
mm/swapfile.c-2141- inc_mm_counter(vma->vm_mm, MM_ANONPAGES);
]
|
| 32/2 |
2026/06/02 16:15 |
llm |
expert |
0m
Model:
Tokens:
input: 17359
output: 32
thoughts: 22
Thoughts: Okay, here's my summary, taking on the persona you requested:
**Navigating the Userfaultfd Landscape**
Alright, let's dive into this. I need to refresh my memory on how `userfaultfd` interacts with memory management. Specifically, I recall there's some interaction with swapping out memory regions, which involves the `MM_SWAPENTS` flag or something similar. Where's that defined? Let's start with a focused search. My instinct tells me this likely resides within the `mm/userfaultfd.c` file. So, I'll initiate a search for `MM_SWAPENTS` within that specific file and see what results emerge. This should give me a solid starting point for understanding how userfaultfd and swapping are related.
|
| 33/2 |
2026/06/02 16:15 |
tool |
grepper |
0m
Error:
Args: map[Expression:MM_SWAPENTS PathPrefix:mm/userfaultfd.c]
Results:
|
| 34/2 |
2026/06/02 16:15 |
llm |
expert |
0m
Model:
Tokens:
input: 17591
output: 32
thoughts: 42
Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience:
**Investigating `MM_SWAPENTS` and `userfaultfd` Interaction**
Hmm, hold on a second. I'm pretty sure `userfaultfd` doesn't directly manipulate `MM_SWAPENTS`. That would be a rather unusual coupling. My immediate thought is to confirm that suspicion and see how memory management handles swap entry structures in `mremap.c`. Let's just go ahead and search the kernel source, specifically within `mm/mremap.c`, and see how the `MM_SWAPENTS` flag is utilized there. I suspect there will be some indirect connection or relationship, but I need to quickly establish if there's a direct write/modify interaction between the two. Need to get the context clear in my head.
|
| 35/2 |
2026/06/02 16:15 |
tool |
grepper |
0m
Error:
Args: map[Expression:MM_SWAPENTS PathPrefix:mm/mremap.c]
Results:
|
| 36/2 |
2026/06/02 16:15 |
llm |
expert |
0m
Model:
Tokens:
input: 17859
output: 30
thoughts: 20
Thoughts: Okay, here's my interpretation of that thought process, assuming I'm an expert in this field, rendered in the first person:
**Navigating the Memory Management Maze**
Alright, so the task at hand involves `MM_SWAPENTS`. My gut tells me this is related to swap entries, probably within the memory management subsystem. Thinking strategically, the most logical place to begin my search would be in `mm/memory.c`. It's the central hub for a lot of core memory management functions, so it's a safe bet to start there and see if `MM_SWAPENTS` is defined or referenced. Hopefully, I'll be able to quickly zero in on where this specific term is used and understand its context within the larger picture of memory management. Time to dive in!
|
| 37/2 |
2026/06/02 16:15 |
tool |
grepper |
0m
Args: map[Expression:MM_SWAPENTS PathPrefix:mm/memory.c]
Results: map[Output:mm/memory.c=938=copy_nonpresent_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
--
mm/memory.c-965- }
mm/memory.c:966: rss[MM_SWAPENTS]++;
mm/memory.c-967- } else if (softleaf_is_migration(entry)) {
--
mm/memory.c=1730=static inline int zap_nonpresent_ptes(struct mmu_gather *tlb,
--
mm/memory.c-1762- nr = swap_pte_batch(pte, max_nr, ptent);
mm/memory.c:1763: rss[MM_SWAPENTS] -= nr;
mm/memory.c-1764- swap_put_entries_direct(entry, nr);
--
mm/memory.c=4706=vm_fault_t do_swap_page(struct vm_fault *vmf)
--
mm/memory.c-5001- add_mm_counter(vma->vm_mm, MM_ANONPAGES, nr_pages);
mm/memory.c:5002: add_mm_counter(vma->vm_mm, MM_SWAPENTS, -nr_pages);
mm/memory.c-5003- pte = mk_pte(page, vma->vm_page_prot);
]
|
| 38/2 |
2026/06/02 16:15 |
llm |
expert |
0m
Model:
Tokens:
input: 18440
output: 33
thoughts: 23
Thoughts: Okay, here's my summary, framed as my internal thought process:
**Diving into the Khugepaged Code**
Alright, I need to understand how huge pages are being swapped out. My first step is to zero in on the relevant code. Given the context of swapping, `MM_SWAPENTS` sounds like a promising keyword to start with. It's almost a guarantee that it'll be used in the `khugepaged` context. Let's see if it’s in `mm/khugepaged.c`. That’s the logical file to start with, given the function. Let’s get searching and see what we can find.
|
| 39/2 |
2026/06/02 16:15 |
tool |
grepper |
0m
Error:
Args: map[Expression:MM_SWAPENTS PathPrefix:mm/khugepaged.c]
Results:
|
| 40/2 |
2026/06/02 16:15 |
llm |
expert |
0m
Model:
Tokens:
input: 18654
output: 32
thoughts: 22
Thoughts: Here's my thought process, as if I were considering this search:
**Diving into Huge Pages: Finding the Right Spot**
Okay, I need to investigate something related to `MM_SWAPENTS`, likely within the realm of huge pages. My first move is clear: I'll head straight for `mm/huge_memory.c`. It's the logical starting point for anything deeply tied to memory management, specifically with a focus on huge pages. I'm expecting to find the definition or usage of `MM_SWAPENTS` within that context. I anticipate this will relate to the swapping behavior of these huge memory areas. The function names and surrounding code will give me the deeper clues that I need to understand what's happening.
|
| 41/2 |
2026/06/02 16:15 |
tool |
grepper |
0m
Error:
Args: map[Expression:MM_SWAPENTS PathPrefix:mm/huge_memory.c]
Results:
|
| 42/2 |
2026/06/02 16:15 |
llm |
expert |
0m
Model:
Tokens:
input: 18879
output: 32
thoughts: 22
Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience:
**Investigating `MM_SWAPENTS` in `mm/madvise.c`**
Alright, time to dive in. I need to refresh my understanding of how `MM_SWAPENTS` is used, specifically within the `madvise.c` module. That's the logical starting point. I'm going to search for the macro definition or any direct uses of `MM_SWAPENTS` within that file first. I'm assuming, given the name, it has something to do with swapping, or potentially tracking swapped-out pages. I'll need to see the contexts of any hits to understand the exact implementation strategy. Is it used to *mark* pages for swapping, to *track* swapped pages, to *control* the swapping process directly, or all of the above? I'll be looking for the underlying data structures it manipulates and how those structures relate to the virtual memory management within the kernel. I anticipate there might be connections to the page table entries, perhaps the page flags, or some dedicated swap-related accounting structures. The goal is to fully grasp the semantic meaning of `MM_SWAPENTS` and how it fits into the broader swapping subsystem. Time to dig in and see what's what.
|
| 43/2 |
2026/06/02 16:15 |
tool |
grepper |
0m
Args: map[Expression:MM_SWAPENTS PathPrefix:mm/madvise.c]
Results: map[Output:mm/madvise.c=651=static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
--
mm/madvise.c-773- if (nr_swap)
mm/madvise.c:774: add_mm_counter(mm, MM_SWAPENTS, nr_swap);
mm/madvise.c-775- if (start_pte) {
]
|
| 44/2 |
2026/06/02 16:15 |
llm |
expert |
0m
Model:
Tokens:
input: 19336
output: 31
thoughts: 21
Thoughts: Here's my thought process, as if I were considering this search:
**Diving into the Swapfile Code**
Okay, so I need to find where `MM_SWAPENTS` is used. My intuition tells me that the place to start is likely within `mm/swapfile.c`. That file is the obvious candidate for handling swap file operations, and `MM_SWAPENTS` is a memory management macro related to swap entries, so it makes sense to look there first. I'll need to use my editor or a tool like `grep` to quickly search the file for that specific string. It's possible it might be defined there, used as a constant, or referenced in a function call. Let's get cracking and see what we can dig up.
|
| 45/2 |
2026/06/02 16:15 |
tool |
grepper |
0m
Args: map[Expression:MM_SWAPENTS PathPrefix:mm/swapfile.c]
Results: map[Output:mm/swapfile.c=2080=static int unuse_pte(struct vm_area_struct *vma, pmd_t *pmd,
--
mm/swapfile.c-2121-
mm/swapfile.c:2122: dec_mm_counter(vma->vm_mm, MM_SWAPENTS);
mm/swapfile.c-2123- if (hwpoisoned) {
--
mm/swapfile.c-2139-
mm/swapfile.c:2140: dec_mm_counter(vma->vm_mm, MM_SWAPENTS);
mm/swapfile.c-2141- inc_mm_counter(vma->vm_mm, MM_ANONPAGES);
]
|
| 46/2 |
2026/06/02 16:15 |
llm |
expert |
0m
Model:
Tokens:
input: 19753
output: 30
thoughts: 20
Thoughts: Okay, here's my summary, tailored as if I'm the one thinking it:
**Digging into Memory Management: Finding the `MM_SWAPENTS` Flag**
Right, let's get down to brass tacks. I need to understand how this `MM_SWAPENTS` flag is being used. This smells like it's crucial for understanding how swap space is managed at the lowest level. My intuition says it's probably related to the page table entries somehow.
My next step is clear: I need to locate this thing's definition and usages. The `mm/rmap.c` file feels like the right starting point, given the context. It's the standard location for rmap implementation, so that's where I'll search. I'm going to fire up my editor/IDE, search for `MM_SWAPENTS` within that file, and see what the code reveals. Time to put on my detective hat and trace the usage. I’m expecting a lot of interaction with the page table entries here, so I'm prepared for some low-level work.
|
| 47/2 |
2026/06/02 16:15 |
tool |
grepper |
0m
Args: map[Expression:MM_SWAPENTS PathPrefix:mm/rmap.c]
Results: map[Output:mm/rmap.c=1964=static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
--
mm/rmap.c-2291- dec_mm_counter(mm, MM_ANONPAGES);
mm/rmap.c:2292: inc_mm_counter(mm, MM_SWAPENTS);
mm/rmap.c-2293- swp_pte = swp_entry_to_pte(entry);
]
|
| 48/2 |
2026/06/02 16:16 |
llm |
expert |
0m
Model:
Tokens:
input: 20174
output: 27
thoughts: 17
Thoughts: Here's my thought process, summarized as you requested:
**Navigating the Memory Management Labyrinth**
Okay, so I need to dig into how the kernel handles file-backed memory. I'm going to start by looking for `MM_FILEPAGES`. That macro, if I recall correctly, is a pretty crucial flag in the `mm_struct` or maybe even the `vm_area_struct`. It's what differentiates pages that come from a file mapping from those that are, say, anonymous. This should point me towards the relevant functions and data structures. I'll focus my search within the `mm/` directory because that's the heart of the memory management subsystem. Expecting some good leads in `mm/filemap.c`, `mm/mmap.c`, or maybe even in `mm/memory.c`. I have a hunch that this flag will appear somewhere in those files. I'm ready to dive into the code and see what insights I can uncover.
|
| 49/2 |
2026/06/02 16:16 |
tool |
grepper |
0m
Args: map[Expression:MM_FILEPAGES PathPrefix:mm/]
Results: map[Output:mm/oom_kill.c=382=static int dump_task(struct task_struct *p, void *arg)
--
mm/oom_kill.c-405- task->tgid, task->mm->total_vm, get_mm_rss_sum(task->mm),
mm/oom_kill.c:406: get_mm_counter_sum(task->mm, MM_ANONPAGES), get_mm_counter_sum(task->mm, MM_FILEPAGES),
mm/oom_kill.c-407- get_mm_counter_sum(task->mm, MM_SHMEMPAGES), mm_pgtables_bytes(task->mm),
--
mm/oom_kill.c=578=static bool oom_reap_task_mm(struct task_struct *tsk, struct mm_struct *mm)
--
mm/oom_kill.c-607- K(get_mm_counter_sum(mm, MM_ANONPAGES)),
mm/oom_kill.c:608: K(get_mm_counter_sum(mm, MM_FILEPAGES)),
mm/oom_kill.c-609- K(get_mm_counter_sum(mm, MM_SHMEMPAGES)));
--
mm/oom_kill.c=928=static void __oom_kill_process(struct task_struct *victim, const char *message)
--
mm/oom_kill.c-963- K(get_mm_counter_sum(mm, MM_ANONPAGES)),
mm/oom_kill.c:964: K(get_mm_counter_sum(mm, MM_FILEPAGES)),
mm/oom_kill.c-965- K(get_mm_counter_sum(mm, MM_SHMEMPAGES)),
]
|
| 50/2 |
2026/06/02 16:16 |
llm |
expert |
0m
Model:
Tokens:
input: 20883
output: 25
thoughts: 48
Thoughts: **Tracing the Source of `MM_FILEPAGES` Updates**
Okay, so I see that `MM_FILEPAGES` is modified somewhere. My initial instinct is to track down the function responsible. And it looks like the comment strongly suggests that it's updated via `add_mm_counter(mm, MM_FILEPAGES, ...)`. That's a good starting point. Now, I need to confirm and understand the call chain. I'll initiate a search for `add_mm_counter.*MM_FILEPAGES`, perhaps using grep or a similar tool, to find all instances where this specific combination appears in the code. This will help me see the contexts in which `MM_FILEPAGES` is being incremented or decremented, and ultimately, understand the logic behind the memory accounting. Let's see what the search reveals... this should give me a clear picture.
|
| 51/2 |
2026/06/02 16:16 |
tool |
grepper |
0m
Error:
Args: map[Expression:add_mm_counter.*MM_FILEPAGES]
Results:
|
| 52/2 |
2026/06/02 16:16 |
llm |
expert |
0m
Model:
Tokens:
input: 21158
output: 25
thoughts: 18
Thoughts: Here's my thought process, as I understand it:
**Digging into Memory Management Counters**
Okay, so I'm looking to track down something specific related to memory management, and I need to be precise in my search. My initial thought is to zero in on a particular counter related to "MM_FILEPAGES." This tells me I'm interested in how file pages are being handled in memory – I'm probably looking at some kind of performance metric or potential bottleneck. I'm going to use a wildcard, `inc_mm_counter.*MM_FILEPAGES`, to maximize my chances of finding the exact counter I need. The `inc_mm_counter` prefix suggests this is an incrementing counter, which is useful for tracking changes over time. The "MM" prefix is likely specific to the memory manager in the system I'm working with. This wildcard should give me a good starting point to explore related counters.
|
| 53/2 |
2026/06/02 16:16 |
tool |
grepper |
0m
Error:
Args: map[Expression:inc_mm_counter.*MM_FILEPAGES]
Results:
|
| 54/2 |
2026/06/02 16:16 |
llm |
expert |
0m
Model:
Tokens:
input: 21412
output: 25
thoughts: 18
Thoughts: Here's my thought process, as if I were considering this query:
**Analyzing the Search Query: `dec_mm_counter.*MM_FILEPAGES`**
Okay, so I see a search query: `dec_mm_counter.*MM_FILEPAGES`. Immediately, my expert brain starts parsing this. The term `dec_mm_counter` jumps out at me. This almost certainly relates to memory management, specifically something decrementing a counter, likely associated with memory usage. The "dec_" prefix is a strong clue towards that. I'd need to know the specific system or environment to be absolutely certain, but this is a solid starting point in most contemporary operating systems and kernels.
The wildcard `.*` is the next thing I focus on. Ah, right. I'm looking for *any* character, zero or more times, *between* the `dec_mm_counter` prefix and the following string. I'm essentially broadening the search, looking for variations or related concepts. Good, efficient use of wildcards there.
And finally, `MM_FILEPAGES`. This is the kicker. That's a very specific, and highly informative, term. This almost certainly tells me the search is focused on the number of file-backed pages in memory. It's often used in systems that use memory mapped files, paging, or virtual memory management, which, you know, is most modern systems. The presence of both `MM` and `FILEPAGES` strongly suggests it will be found in Linux/Unix operating systems, and is most likely a system-level variable or macro relating to file-backed memory pages in the kernel.
So, summing it up, the query appears to be geared toward understanding the activity of a counter that is decreasing (likely due to a deallocation event) related to the management of file-backed pages in memory. The wildcard is an effort to cast the net wide to account for different naming conventions or slight variations in how this data is stored or reported. I'd be looking for definitions, documentation, or code snippets that contain variations on that theme. I'll need to use this information to determine the search context so I can be more specific, but this seems like a solid foundation for finding what I'm looking for.
|
| 55/2 |
2026/06/02 16:16 |
tool |
grepper |
0m
Error:
Args: map[Expression:dec_mm_counter.*MM_FILEPAGES]
Results:
|
| 56/2 |
2026/06/02 16:16 |
llm |
expert |
0m
Model:
Tokens:
input: 21945
output: 34
thoughts: 38
Thoughts: **Investigating the Update Mechanism of `MM_FILEPAGES`**
Okay, so I'm looking at something and I've hit a snag. The core question right now is how the system actually updates `MM_FILEPAGES`. I need to figure out the mechanisms involved. Time to dig in! The best place to start, given its definition, is the header file where this macro is declared, `include/linux/mm_types.h`. I'll open that up and search for `MM_FILEPAGES` directly. That should give me the initial context and, hopefully, clues about the associated structures or functions that are manipulating this value. From there, I'll be able to work backwards and trace the update path. Let's see what we can find.
|
| 57/2 |
2026/06/02 16:16 |
tool |
grepper |
0m
Error:
Args: map[Expression:MM_FILEPAGES PathPrefix:include/linux/mm_types.h]
Results:
|
| 58/2 |
2026/06/02 16:16 |
llm |
expert |
0m
Model:
Tokens:
input: 22196
output: 36
thoughts: 26
Thoughts: Okay, here's my take on those thoughts, framed as an expert's internal monologue:
**Investigating `MM_FILEPAGES` in `mm_types_task.h`**
Alright, let's dive in. I need to refresh my memory on how this is structured and where the relevant definitions reside. The goal is to understand the implications of `MM_FILEPAGES`, so I'll start by looking for it directly in `include/linux/mm_types_task.h`. That header's a logical place to begin; it should contain the fundamental structures related to memory management and how tasks interact with those structures, especially considering the "types" aspect of the filename. Hopefully, the definition or an insightful usage example will be clearly presented there. Understanding how `MM_FILEPAGES` is *used* is probably more important than the definition itself, so I'll be looking for context. I'm expecting to find it tied to file-backed memory regions, naturally. Knowing this is a macro makes things a bit simpler, so I'll expect to look at how it's used within other macros or data structures within that header file. That should give me a good jumping-off point to see if the structure, if relevant, has changed or been updated.
|
| 59/2 |
2026/06/02 16:16 |
tool |
grepper |
0m
Args: map[Expression:MM_FILEPAGES PathPrefix:include/linux/mm_types_task.h]
Results: map[Output:include/linux/mm_types_task.h=26=enum {
include/linux/mm_types_task.h:27: MM_FILEPAGES, /* Resident file mapping pages */
include/linux/mm_types_task.h-28- MM_ANONPAGES, /* Resident anonymous pages */
]
|
| 60/2 |
2026/06/02 16:16 |
llm |
expert |
0m
Model:
Tokens:
input: 22619
output: 27
thoughts: 17
Thoughts: Okay, here's my summarized thought process, presented as if I were thinking it:
**Digging into Memory Management**
Alright, I need to investigate something related to memory management. Let's start with a specific file, `mm_counter_file`. The naming convention suggests it resides within the broader `mm/` directory, which is a standard location for memory management related code. So, that's where I'll initiate my search. I'll need to locate this file to understand its role and how it's being utilized within the system's memory management routines. Knowing what's in this file will provide the necessary context to further refine my investigation.
|
| 61/2 |
2026/06/02 16:16 |
tool |
grepper |
0m
Args: map[Expression:mm_counter_file PathPrefix:mm/]
Results: map[Output:mm/filemap.c=3866=vm_fault_t filemap_map_pages(struct vm_fault *vmf,
--
mm/filemap.c-3909-
mm/filemap.c:3910: folio_type = mm_counter_file(folio);
mm/filemap.c-3911- do {
--
mm/huge_memory.c=1526=static vm_fault_t insert_pmd(struct vm_area_struct *vma, unsigned long addr,
--
mm/huge_memory.c-1569- folio_add_file_rmap_pmd(fop.folio, &fop.folio->page, vma);
mm/huge_memory.c:1570: add_mm_counter(mm, mm_counter_file(fop.folio), HPAGE_PMD_NR);
mm/huge_memory.c-1571- }
--
mm/huge_memory.c=1658=static vm_fault_t insert_pud(struct vm_area_struct *vma, unsigned long addr,
--
mm/huge_memory.c-1688- folio_add_file_rmap_pud(fop.folio, &fop.folio->page, vma);
mm/huge_memory.c:1689: add_mm_counter(mm, mm_counter_file(fop.folio), HPAGE_PUD_NR);
mm/huge_memory.c-1690- } else {
--
mm/huge_memory.c=2344=int zap_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,
--
mm/huge_memory.c-2399- zap_deposited_table(tlb->mm, pmd);
mm/huge_memory.c:2400: add_mm_counter(tlb->mm, mm_counter_file(folio),
mm/huge_memory.c-2401- -HPAGE_PMD_NR);
--
mm/huge_memory.c=2853=int zap_huge_pud(struct mmu_gather *tlb, struct vm_area_struct *vma,
--
mm/huge_memory.c-2879- folio_remove_rmap_pud(folio, page, vma);
mm/huge_memory.c:2880: add_mm_counter(tlb->mm, mm_counter_file(folio), -HPAGE_PUD_NR);
mm/huge_memory.c-2881-
--
mm/huge_memory.c=2888=static void __split_huge_pud_locked(struct vm_area_struct *vma, pud_t *pud,
--
mm/huge_memory.c-2915- folio_put(folio);
mm/huge_memory.c:2916: add_mm_counter(vma->vm_mm, mm_counter_file(folio),
mm/huge_memory.c-2917- -HPAGE_PUD_NR);
--
mm/huge_memory.c=2987=static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,
--
mm/huge_memory.c-3034- }
mm/huge_memory.c:3035: add_mm_counter(mm, mm_counter_file(folio), -HPAGE_PMD_NR);
mm/huge_memory.c-3036- return;
--
mm/khugepaged.c=1463=static enum scan_result try_collapse_pte_mapped_thp(struct mm_struct *mm, unsigned long addr,
--
mm/khugepaged.c-1634- folio_ref_sub(folio, nr_mapped_ptes);
mm/khugepaged.c:1635: add_mm_counter(mm, mm_counter_file(folio), -nr_mapped_ptes);
mm/khugepaged.c-1636- }
--
mm/khugepaged.c-1670- folio_ref_sub(folio, nr_mapped_ptes);
mm/khugepaged.c:1671: add_mm_counter(mm, mm_counter_file(folio), -nr_mapped_ptes);
mm/khugepaged.c-1672- }
--
mm/memory.c=1126=copy_present_ptes(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
--
mm/memory.c-1163- folio_dup_file_rmap_ptes(folio, page, nr, dst_vma);
mm/memory.c:1164: rss[mm_counter_file(folio)] += nr;
mm/memory.c-1165- }
--
mm/memory.c-1189- folio_dup_file_rmap_pte(folio, page, dst_vma);
mm/memory.c:1190: rss[mm_counter_file(folio)]++;
mm/memory.c-1191- }
--
mm/memory.c=2341=static int insert_page_into_pte_locked(struct vm_area_struct *vma, pte_t *pte,
--
mm/memory.c-2374- }
mm/memory.c:2375: inc_mm_counter(vma->vm_mm, mm_counter_file(folio));
mm/memory.c-2376- folio_add_file_rmap_pte(folio, page, vma);
--
mm/memory.c=3758=static vm_fault_t wp_page_copy(struct vm_fault *vmf)
--
mm/memory.c-3819- if (!folio_test_anon(old_folio)) {
mm/memory.c:3820: dec_mm_counter(mm, mm_counter_file(old_folio));
mm/memory.c-3821- inc_mm_counter(mm, MM_ANONPAGES);
--
mm/memory.c=5406=vm_fault_t do_set_pmd(struct vm_fault *vmf, struct folio *folio, struct page *page)
--
mm/memory.c-5460-
mm/memory.c:5461: add_mm_counter(vma->vm_mm, mm_counter_file(folio), HPAGE_PMD_NR);
mm/memory.c-5462- folio_add_file_rmap_pmd(folio, page, vma);
--
mm/memory.c=5555=vm_fault_t finish_fault(struct vm_fault *vmf)
--
mm/memory.c-5662- set_pte_range(vmf, folio, page, nr_pages, addr);
mm/memory.c:5663: type = is_cow ? MM_ANONPAGES : mm_counter_file(folio);
mm/memory.c-5664- add_mm_counter(vma->vm_mm, type, nr_pages);
--
mm/rmap.c=1964=static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
--
mm/rmap.c-2319- */
mm/rmap.c:2320: add_mm_counter(mm, mm_counter_file(folio), -nr_pages);
mm/rmap.c-2321- }
]
|
| 62/2 |
2026/06/02 16:16 |
llm |
expert |
0m
Model:
Tokens:
input: 24737
output: 30
thoughts: 169
Thoughts: **Analyzing a Memory Leak Report**
Okay, so I'm looking at this memory leak report and the first thing that jumps out is how `MM_FILEPAGES` is being updated – seems to be through `mm_counter_file(folio)`. But hang on, `mm_counter_file(folio)` *returns* either `MM_SHMEMPAGES` or `MM_FILEPAGES`, right? The report states `MM_FILEPAGES val:1` and `MM_SWAPENTS val:32`. Right, so this means one file page is leaked, and we've got a leak of 32 swap entries. Hmmm, 32 swap entries... that's exactly 128KB, assuming 4KB pages.
That's an interesting correlation. This rings a bell. Could this have something to do with that `uprobe` issue? I recall that bug report had `MM_FILEPAGES val:-1`, which meant file pages were *being uncounted*. I think this warrants a deeper dive and let's go back over those searches for "Bad rss-counter state" and "MM_FILEPAGES" again.
|
| 63/2 |
2026/06/02 16:16 |
tool |
git-log |
0m
Args: map[MessageRegexps:[Bad rss-counter state MM_FILEPAGES]]
Results: map[Output:bddf10d26e6e uprobes: Reject the shared zeropage in uprobe_write_opcode()
542db12a9c42 powerpc: Fix random segfault when freeing hugetlb range
8495f7e6732e fork: improve error message for corrupted page tables
]
|
| 64/2 |
2026/06/02 16:17 |
llm |
expert |
0m
Model:
Tokens:
input: 25276
output: 26
thoughts: 30
Thoughts: Here's my take on those thoughts:
**Analyzing a Potential Kernel Bug Fix**
Okay, so I see a commit message that immediately piques my interest: `542db12a9c42 powerpc: Fix random segfault when freeing hugetlb range`. Immediately, I'm thinking, "PowerPC architecture? Huge pages and memory management? Segfaults? This has the potential to be interesting...and likely painful." My mind starts running through the possible root causes. Was this a race condition? A double-free? Perhaps an off-by-one error in calculating the HugeTLB range boundaries during deallocation? Or maybe it’s a corruption issue tied to cache coherency. Given my experience, I can already feel the complexity of memory management on PowerPC, especially when dealing with large pages. I want to dive into the code. This is definitely one I need to look closer at.
|
| 65/2 |
2026/06/02 16:17 |
tool |
git-show |
0m
Args:
Results: map[Output:commit 542db12a9c42d1ce70c45091765e02f74c129f43
Author: Christophe Leroy <chleroy@kernel.org>
Date: Mon Aug 31 07:58:19 2020 +0000
powerpc: Fix random segfault when freeing hugetlb range
The following random segfault is observed from time to time with
map_hugetlb selftest:
root@localhost:~# ./map_hugetlb 1 19
524288 kB hugepages
Mapping 1 Mbytes
Segmentation fault
[ 31.219972] map_hugetlb[365]: segfault (11) at 117 nip 77974f8c lr 779a6834 code 1 in ld-2.23.so[77966000+21000]
[ 31.220192] map_hugetlb[365]: code: 9421ffc0 480318d1 93410028 90010044 9361002c 93810030 93a10034 93c10038
[ 31.220307] map_hugetlb[365]: code: 93e1003c 93210024 8123007c 81430038 <80e90004> 814a0004 7f443a14 813a0004
[ 31.221911] BUG: Bad rss-counter state mm:(ptrval) type:MM_FILEPAGES val:33
[ 31.229362] BUG: Bad rss-counter state mm:(ptrval) type:MM_ANONPAGES val:5
This fault is due to hugetlb_free_pgd_range() freeing page tables
that are also used by regular pages.
As explain in the comment at the beginning of
hugetlb_free_pgd_range(), the verification done in free_pgd_range()
on floor and ceiling is not done here, which means
hugetlb_free_pte_range() can free outside the expected range.
As the verification cannot be done in hugetlb_free_pgd_range(), it
must be done in hugetlb_free_pte_range().
Fixes: b250c8c08c79 ("powerpc/8xx: Manage 512k huge pages as standard pages.")
Cc: stable@vger.kernel.org
Signed-off-by: Christophe Leroy <christophe.leroy@csgroup.eu>
Reviewed-by: Aneesh Kumar K.V <aneesh.kumar@linux.ibm.com>
Signed-off-by: Michael Ellerman <mpe@ellerman.id.au>
Link: https://lore.kernel.org/r/f0cb2a5477cd87d1eaadb128042e20aeb2bc2859.1598860677.git.christophe.leroy@csgroup.eu
diff --git a/arch/powerpc/mm/hugetlbpage.c b/arch/powerpc/mm/hugetlbpage.c
index 26292544630f..e7ae2a2c4545 100644
--- a/arch/powerpc/mm/hugetlbpage.c
+++ b/arch/powerpc/mm/hugetlbpage.c
@@ -330,10 +330,24 @@ static void free_hugepd_range(struct mmu_gather *tlb, hugepd_t *hpdp, int pdshif
get_hugepd_cache_index(pdshift - shift));
}
-static void hugetlb_free_pte_range(struct mmu_gather *tlb, pmd_t *pmd, unsigned long addr)
+static void hugetlb_free_pte_range(struct mmu_gather *tlb, pmd_t *pmd,
+ unsigned long addr, unsigned long end,
+ unsigned long floor, unsigned long ceiling)
{
+ unsigned long start = addr;
pgtable_t token = pmd_pgtable(*pmd);
+ start &= PMD_MASK;
+ if (start < floor)
+ return;
+ if (ceiling) {
+ ceiling &= PMD_MASK;
+ if (!ceiling)
+ return;
+ }
+ if (end - 1 > ceiling - 1)
+ return;
+
pmd_clear(pmd);
pte_free_tlb(tlb, token, addr);
mm_dec_nr_ptes(tlb->mm);
@@ -363,7 +377,7 @@ static void hugetlb_free_pmd_range(struct mmu_gather *tlb, pud_t *pud,
*/
WARN_ON(!IS_ENABLED(CONFIG_PPC_8xx));
- hugetlb_free_pte_range(tlb, pmd, addr);
+ hugetlb_free_pte_range(tlb, pmd, addr, end, floor, ceiling);
continue;
}
]
|
| 66/2 |
2026/06/02 16:17 |
llm |
expert |
0m
Model:
Tokens:
input: 26933
output: 30
thoughts: 88
Thoughts: **PowerPC Bug Hunt Begins**
Okay, so this is definitely a PowerPC-specific problem. My immediate instinct is to check the syzbot database – that's usually the quickest way to find existing reports and potential clues. I was going to look for mentions of `MM_SWAPENTS` and `MM_FILEPAGES`, those are usually good indicators of swap-related issues. Dang, I forgot I don't have direct access to syzbot reports from here. No worries, I can still use the kernel code and git logs. Right, so now the plan is to search the git log for commits that mention *both* `MM_SWAPENTS` and `MM_FILEPAGES` in the same commit message. That should narrow it down significantly. Let's see what that uncovers.
|
| 67/2 |
2026/06/02 16:17 |
tool |
git-log |
0m
Args: map[MessageRegexps:[MM_SWAPENTS MM_FILEPAGES]]
Results:
|
| 68/2 |
2026/06/02 16:17 |
llm |
expert |
0m
Model:
Tokens:
input: 27236
output: 30
thoughts: 25
Thoughts: Okay, here's my take on those thoughts, framed as an expert's internal monologue:
**Investigating a Suspected Memory Management Bug**
Right, let's get down to brass tacks. I need to dig into this potential memory management issue. The fact that I'm thinking about searching for both `MM_SWAPENTS` and a "Bad rss-counter state" within the same commit immediately sets off alarm bells. That combination strongly suggests a problem related to how the kernel is tracking memory usage and swapping. `MM_SWAPENTS`, specifically, often flags code touching swap entry bookkeeping. A bad RSS counter state is a classic symptom of either a double-accounting problem – where memory is counted twice – or, conversely, a memory leak or other corruption leading to an incorrect view of how much memory a process actually *holds*. My immediate thought is, let's see how these two concepts might intersect, and determine how these may have been found together. I'm imagining a situation where some accounting code isn't correctly updating swap entry counters, which then results in the RSS counter being wrong. That's a prime suspect. Let's start the search and see what the commit history tells us. This should give me some context, and point me in the right direction.
|
| 69/2 |
2026/06/02 16:17 |
tool |
git-log |
0m
Args: map[MessageRegexps:[MM_SWAPENTS Bad rss-counter state]]
Results:
|
| 70/2 |
2026/06/02 16:17 |
llm |
expert |
0m
Model:
Tokens:
input: 27572
output: 22
thoughts: 16
Thoughts: Here's my thought process, summarized as you requested:
**Investigating a Code Issue: The Hunt for MM_SWAPENTS**
Okay, time to get to the bottom of this. I'm thinking, let's start with the most obvious approach. I need to understand the history of `MM_SWAPENTS` within the codebase. My instinct is that if there's an issue with it, the git log will provide the breadcrumbs. So, I'm going to start by searching the git logs directly for that specific term. This should allow me to see when it was introduced, how it's been modified over time, and if there are any related commits that might shed light on the problem I'm investigating. I expect this will give me a clear baseline for further analysis.
|
| 71/2 |
2026/06/02 16:17 |
tool |
git-log |
0m
Args: map[MessageRegexps:[MM_SWAPENTS]]
Results: map[Output:3c77f8457221 exec: make argv/envp memory visible to oom-killer
]
|
| 72/2 |
2026/06/02 16:18 |
llm |
expert |
0m
Model:
Tokens:
input: 27818
output: 27
thoughts: 32
Thoughts: Okay, here's my interpretation of that thought process, assuming I'm already deep in the weeds of kernel debugging:
**Analyzing the OOM Killer's Visibility of Process Memory**
Alright, I need to dive into this commit – `3c77f8457221 exec: make argv/envp memory visible to oom-killer`. My gut tells me this relates to how the out-of-memory (OOM) killer behaves when a process is starving for memory and needs to be terminated. Specifically, I'm thinking about how the arguments (`argv`) and environment variables (`envp`) of a process are handled, and whether the OOM killer is *aware* of their memory footprint.
We all know the OOM killer makes a decision about what process to kill based on a number of factors, including its memory usage. Now, I suspect this commit is addressing the case where the memory allocated for `argv` and `envp` wasn't being correctly factored into those calculations. If the OOM killer *isn't* aware of the full picture – the size of the argument and environment space – it could make an uninformed decision and kill the wrong process, or not kill anything when it should. Potentially, it could mean that a process with a smaller data segment, but a larger `argv`/`envp` footprint, could be unfairly targeted. That's a serious bug!
So, I need to see *exactly* how this change affects the visibility of those `argv` and `envp` memory regions to the OOM killer. I'll need to understand the prior code flow, what's been changed, and how the OOM killer's calculations are impacted. I'm also betting this has important implications for containerized environments, which often have complex `argv`/`envp` configurations.
|
| 73/2 |
2026/06/02 16:18 |
tool |
git-show |
0m
Args:
Results: map[Output:commit 3c77f845722158206a7209c45ccddc264d19319c
Author: Oleg Nesterov <oleg@redhat.com>
Date: Tue Nov 30 20:55:34 2010 +0100
exec: make argv/envp memory visible to oom-killer
Brad Spengler published a local memory-allocation DoS that
evades the OOM-killer (though not the virtual memory RLIMIT):
http://www.grsecurity.net/~spender/64bit_dos.c
execve()->copy_strings() can allocate a lot of memory, but
this is not visible to oom-killer, nobody can see the nascent
bprm->mm and take it into account.
With this patch get_arg_page() increments current's MM_ANONPAGES
counter every time we allocate the new page for argv/envp. When
do_execve() succeds or fails, we change this counter back.
Technically this is not 100% correct, we can't know if the new
page is swapped out and turn MM_ANONPAGES into MM_SWAPENTS, but
I don't think this really matters and everything becomes correct
once exec changes ->mm or fails.
Reported-by: Brad Spengler <spender@grsecurity.net>
Reviewed-and-discussed-by: KOSAKI Motohiro <kosaki.motohiro@jp.fujitsu.com>
Signed-off-by: Oleg Nesterov <oleg@redhat.com>
Cc: stable@kernel.org
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
diff --git a/fs/exec.c b/fs/exec.c
index 99d33a1371e9..4303b9035fe7 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -164,6 +164,25 @@ SYSCALL_DEFINE1(uselib, const char __user *, library)
#ifdef CONFIG_MMU
+static void acct_arg_size(struct linux_binprm *bprm, unsigned long pages)
+{
+ struct mm_struct *mm = current->mm;
+ long diff = (long)(pages - bprm->vma_pages);
+
+ if (!mm || !diff)
+ return;
+
+ bprm->vma_pages = pages;
+
+#ifdef SPLIT_RSS_COUNTING
+ add_mm_counter(mm, MM_ANONPAGES, diff);
+#else
+ spin_lock(&mm->page_table_lock);
+ add_mm_counter(mm, MM_ANONPAGES, diff);
+ spin_unlock(&mm->page_table_lock);
+#endif
+}
+
static struct page *get_arg_page(struct linux_binprm *bprm, unsigned long pos,
int write)
{
@@ -186,6 +205,8 @@ static struct page *get_arg_page(struct linux_binprm *bprm, unsigned long pos,
unsigned long size = bprm->vma->vm_end - bprm->vma->vm_start;
struct rlimit *rlim;
+ acct_arg_size(bprm, size / PAGE_SIZE);
+
/*
* We've historically supported up to 32 pages (ARG_MAX)
* of argument strings even with small stacks
@@ -276,6 +297,10 @@ static bool valid_arg_len(struct linux_binprm *bprm, long len)
#else
+static inline void acct_arg_size(struct linux_binprm *bprm, unsigned long pages)
+{
+}
+
static struct page *get_arg_page(struct linux_binprm *bprm, unsigned long pos,
int write)
{
@@ -1003,6 +1028,7 @@ int flush_old_exec(struct linux_binprm * bprm)
/*
* Release all of the old mmap stuff
*/
+ acct_arg_size(bprm, 0);
retval = exec_mmap(bprm->mm);
if (retval)
goto out;
@@ -1426,8 +1452,10 @@ int do_execve(const char * filename,
return retval;
out:
- if (bprm->mm)
- mmput (bprm->mm);
+ if (bprm->mm) {
+ acct_arg_size(bprm, 0);
+ mmput(bprm->mm);
+ }
out_file:
if (bprm->file) {
diff --git a/include/linux/binfmts.h b/include/linux/binfmts.h
index a065612fc928..7c87796d20da 100644
--- a/include/linux/binfmts.h
+++ b/include/linux/binfmts.h
@@ -29,6 +29,7 @@ struct linux_binprm{
char buf[BINPRM_BUF_SIZE];
#ifdef CONFIG_MMU
struct vm_area_struct *vma;
+ unsigned long vma_pages;
#else
# define MAX_ARG_PAGES 32
struct page *page[MAX_ARG_PAGES];
]
|
| 74/2 |
2026/06/02 16:18 |
llm |
expert |
0m
Model:
Tokens:
input: 29688
output: 36
thoughts: 179
Thoughts: **Investigating a Suspected "Bad rss-counter state"**
Okay, this feels ancient, like I'm wading into some deep, dusty code. First instinct is to jump straight to kernel bugzilla or syzbot and search for "Bad rss-counter state." Damn, no access right now. Okay, fine. Let's think this through from first principles. How could `MM_FILEPAGES` or `MM_SWAPENTS` possibly leak or get corrupted? I'm picturing the memory management landscape in my head...
Wait... if a file page is *swapped out*, does that *become* a swap entry? No, no, that's not right. File pages aren't typically swapped; they're written back to their files and discarded. Only anonymous pages and shared memory pages go into swap space. Right, right...
Ah, right! Shared memory pages are accounted as `MM_SHMEMPAGES`. That's the key. Now, hold on... `mm_counter_file(folio)`... that's where the magic happens. It returns `MM_SHMEMPAGES` if `folio_test_swapbacked(folio)` is true, otherwise, it returns `MM_FILEPAGES`. I really need to double-check that `mm_counter_file` function... Let's go look.
|
| 75/2 |
2026/06/02 16:18 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:include/linux/mm.h Name:mm_counter_file]
Results: map[SourceCode:3096: /* Optimized variant when folio is already known not to be anon */
3097: static inline int mm_counter_file(struct folio *folio)
3098: {
3099: if (folio_test_swapbacked(folio))
3100: return MM_SHMEMPAGES;
3101: return MM_FILEPAGES;
3102: }
SourceFile:include/linux/mm.h]
|
| 76/2 |
2026/06/02 16:18 |
llm |
expert |
0m
Model:
Tokens:
input: 30325
output: 25
thoughts: 192
Thoughts: **Investigating a Memory Leak**
Okay, so I'm looking at this bug report and the memory accounting seems a bit off. They're reporting a leak of one `MM_FILEPAGES` and thirty-two `MM_SWAPENTS`. `MM_FILEPAGES`, right, that's for file-backed, non-swap memory, and `MM_SWAPENTS` is, of course, for swap entries. The discrepancy is throwing me off. How can a process have a leak in both categories, and with those specific numbers? It's not a typical pattern I'd expect.
Could this be related to something recent? My mind immediately jumps to `uprobe`, as it's often a source of memory-related issues. I'm going to quickly re-examine that `uprobe` commit, the one with that `bddf10d26e6e` hash – "uprobes: Reject the shared zeropage in `uprobe_write_opcode()`". Wait a minute… wasn't the original `uprobe` bug *also* showing `MM_FILEPAGES`, but with a negative value? This is intriguing.
Maybe there's a different, related bug that's presenting with a positive `MM_FILEPAGES` value? I should investigate this further. I'm going to start by doing a quick search for instances of both `MM_FILEPAGES` and `MM_SWAPENTS` within the `mm/` directory. That's a good starting point to see what's going on. I'll see if I can find some clues there.
|
| 77/2 |
2026/06/02 16:18 |
tool |
grepper |
0m
Error:
Args: map[Expression:MM_FILEPAGES.*MM_SWAPENTS]
Results:
|
| 78/2 |
2026/06/02 16:18 |
llm |
expert |
0m
Model:
Tokens:
input: 30904
output: 34
thoughts: 24
Thoughts: Okay, here's my attempt at summarizing that thought process, keeping in mind I'm speaking to a fellow expert:
**Investigating a Kernel Probe Point**
Alright, so I'm looking into something and my mind immediately jumps to `MM_SWAPENTS`. It's likely a memory management or swapping-related event I'm interested in, and my first instinct is to see how it's handled, or even defined, within the kernel's event tracing framework. That means my next step is a targeted search for it within `kernel/events/uprobes.c`. That file is where I'd expect to find details about how user-space probes interact with memory-related events – or at the very least, a likely point of interest within that broader section of the kernel. The goal is to figure out if it's there and what I can learn from its context. From there, I'll be off and running.
|
| 79/2 |
2026/06/02 16:18 |
tool |
grepper |
0m
Error:
Args: map[Expression:MM_SWAPENTS PathPrefix:kernel/events/uprobes.c]
Results:
|
| 80/2 |
2026/06/02 16:18 |
llm |
expert |
0m
Model:
Tokens:
input: 31176
output: 34
thoughts: 24
Thoughts: Okay, here's my attempt at summarizing that thought process, keeping in mind I'm speaking to a fellow expert:
**Investigating Uprobes and Memory Mapping**
Alright, I'm thinking I need to dig into the relationship between uprobes and memory mapping, specifically focusing on how they interact with file-backed pages. My immediate thought is to check `kernel/events/uprobes.c`. I recall that file being the central hub for uprobe functionality. I figure that file is the right place to start looking for the definition or references to `MM_FILEPAGES`. I want to understand how it is handled there, especially when an uprobe is involved. Hopefully, that will give me a clear picture of what's happening under the hood. Time to fire up the editor and start grepping!
|
| 81/2 |
2026/06/02 16:18 |
tool |
grepper |
0m
Error:
Args: map[Expression:MM_FILEPAGES PathPrefix:kernel/events/uprobes.c]
Results:
|
| 82/2 |
2026/06/02 16:18 |
llm |
expert |
0m
Model:
Tokens:
input: 31419
output: 37
thoughts: 54
Thoughts: Here's my thought process, as it stands:
**Digging into uprobe_write_opcode**
Okay, so `uprobe_write_opcode`… right, it's using `get_user_page_vma_remote` and then, crucially, `__replace_page`. That's the bit that's jumping out at me as a potential area of interest for analysis, in relation to our overall project. I'm going to quickly check the definition of `__replace_page`. Right, that function's defined in `kernel/events/uprobes.c`. Let's have a look. I'll take a quick scan for anything immediately concerning in that file.
|
| 83/2 |
2026/06/02 16:18 |
tool |
codesearch-definition-source |
0m
Error:
Args: map[ContextFile:kernel/events/uprobes.c Name:__replace_page]
Results: map[SourceCode: SourceFile:]
|
| 84/2 |
2026/06/02 16:18 |
llm |
expert |
0m
Model:
Tokens:
input: 31682
output: 26
thoughts: 16
Thoughts: Here's my thought process, as if I were considering this search:
**Diving into the `mm/` Codebase**
Okay, so I need to find where `__replace_page` is used, and my instinct is to look within the `mm/` directory. That's the memory management subsystem, so it's the most logical place to start. This is a common pattern in kernel development – you know where a function logically belongs and that guides your search. I'm assuming this is a kernel internal function, likely related to page table manipulation or page replacement. Let's see what the search reveals. It's time to fire up my tools and get to work.
|
| 85/2 |
2026/06/02 16:19 |
tool |
grepper |
0m
Error:
Args: map[Expression:__replace_page PathPrefix:mm/]
Results:
|
| 86/2 |
2026/06/02 16:19 |
llm |
expert |
0m
Model:
Tokens:
input: 31884
output: 33
thoughts: 23
Thoughts: Okay, here's my summary, presented as a stream of thought:
**Finding the Right Spot in the Uprobes Code**
Right, time to dive in. I need to find where the `__replace_page` function is used within the kernel's uprobes code. The `kernel/events/uprobes.c` file is the logical place to start – it's where much of the uprobe magic happens. So, I'll initiate a search for `__replace_page` within that specific file. This should quickly pinpoint the locations where this function is called, allowing me to understand its role and context within the uprobes mechanism. Knowing its usage is key to understanding the bigger picture. Let's see what the search uncovers.
|
| 87/2 |
2026/06/02 16:19 |
tool |
grepper |
0m
Args: map[Expression:__replace_page PathPrefix:kernel/events/uprobes.c]
Results: map[Output:kernel/events/uprobes.c=1151=static int install_breakpoint(struct uprobe *uprobe, struct vm_area_struct *vma,
--
kernel/events/uprobes.c-1163- * set MMF_HAS_UPROBES in advance for uprobe_pre_sstep_notifier(),
kernel/events/uprobes.c:1164: * the task can hit this breakpoint right after __replace_page().
kernel/events/uprobes.c-1165- */
]
|
| 88/2 |
2026/06/02 16:19 |
llm |
expert |
0m
Model:
Tokens:
input: 32242
output: 39
thoughts: 38
Thoughts: Okay, here's the summary, rendered as a first-person thought process, keeping in mind the user is an expert.
**The `__replace_page` Mystery**
Wait, hold on a second… did they rename or, even worse, completely remove `__replace_page`? That's not good. That function's pretty fundamental, and if it's gone, things are going to be more complicated. Let's see... I need to track down where this change originated. My first thought is to check the `uprobe_write_opcode` function. It looks like it's in `kernel/events/uprobes.c`, which makes sense given its relation to probing. I'll need to dig into the recent commits there and see if this function is used and where. Perhaps the rename is something local to that file, or maybe the function's functionality has been merged elsewhere. Let's start the investigation there.
|
| 89/2 |
2026/06/02 16:19 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/events/uprobes.c Name:uprobe_write_opcode]
Results: map[SourceCode: 471: /*
472: * NOTE:
473: * Expect the breakpoint instruction to be the smallest size instruction for
474: * the architecture. If an arch has variable length instruction and the
475: * breakpoint instruction is not of the smallest length instruction
476: * supported by that architecture then we need to modify is_trap_at_addr and
477: * uprobe_write_opcode accordingly. This would never be a problem for archs
478: * that have fixed length instructions.
479: *
480: * uprobe_write_opcode - write the opcode at a given virtual address.
481: * @auprobe: arch specific probepoint information.
482: * @vma: the probed virtual memory area.
483: * @opcode_vaddr: the virtual address to store the opcode.
484: * @opcode: opcode to be written at @opcode_vaddr.
485: *
486: * Called with mm->mmap_lock held for write.
487: * Return 0 (success) or a negative errno.
488: */
489: int uprobe_write_opcode(struct arch_uprobe *auprobe, struct vm_area_struct *vma,
490: const unsigned long opcode_vaddr, uprobe_opcode_t opcode,
491: bool is_register)
492: {
493: return uprobe_write(auprobe, vma, opcode_vaddr, &opcode, UPROBE_SWBP_INSN_SIZE,
494: verify_opcode, is_register, true /* do_update_ref_ctr */, NULL);
495: }
SourceFile:kernel/events/uprobes.c]
|
| 90/2 |
2026/06/02 16:19 |
llm |
expert |
0m
Model:
Tokens:
input: 33011
output: 37
thoughts: 22
Thoughts: Here's my take on those thoughts, tailored for an expert audience:
**Diving into `uprobe_write`**
Okay, so I need to refresh my memory on how this all works. Let's head straight to the source. Specifically, I need to look into `uprobe_write` within `kernel/events/uprobes.c`. I know this is the function responsible for handling the actual writing of the probe data – probably things like manipulating the instruction stream or setting up breakpoints when a user-space probe is triggered. I'm guessing it's dealing with the core logic of patching the target instructions and ensuring everything is aligned correctly. I'll need to pay close attention to the error handling and any interactions it might have with the various probe types, especially how it handles multiple probes on the same address. I'm also curious about any potential race conditions and how they are addressed. Let's see if it’s using any locking, atomic operations, or maybe even RCU to guarantee integrity and safety. Time to dig in and remind myself of the current state of uprobes implementation and its potential quirks.
|
| 91/2 |
2026/06/02 16:19 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/events/uprobes.c Name:uprobe_write]
Results: map[SourceCode: 497: int uprobe_write(struct arch_uprobe *auprobe, struct vm_area_struct *vma,
498: const unsigned long insn_vaddr, uprobe_opcode_t *insn, int nbytes,
499: uprobe_write_verify_t verify, bool is_register, bool do_update_ref_ctr,
500: void *data)
501: {
502: const unsigned long vaddr = insn_vaddr & PAGE_MASK;
503: struct mm_struct *mm = vma->vm_mm;
504: struct uprobe *uprobe;
505: int ret, ref_ctr_updated = 0;
506: unsigned int gup_flags = FOLL_FORCE;
507: struct mmu_notifier_range range;
508: struct folio_walk fw;
509: struct folio *folio;
510: struct page *page;
511:
512: uprobe = container_of(auprobe, struct uprobe, arch);
513:
514: if (WARN_ON_ONCE(!is_cow_mapping(vma->vm_flags)))
515: return -EINVAL;
516:
517: /*
518: * When registering, we have to break COW to get an exclusive anonymous
519: * page that we can safely modify. Use FOLL_WRITE to trigger a write
520: * fault if required. When unregistering, we might be lucky and the
521: * anon page is already gone. So defer write faults until really
522: * required. Use FOLL_SPLIT_PMD, because __uprobe_write()
523: * cannot deal with PMDs yet.
524: */
525: if (is_register)
526: gup_flags |= FOLL_WRITE | FOLL_SPLIT_PMD;
527:
528: retry:
529: ret = get_user_pages_remote(mm, vaddr, 1, gup_flags, &page, NULL);
530: if (ret <= 0)
531: goto out;
532: folio = page_folio(page);
533:
534: ret = verify(page, insn_vaddr, insn, nbytes, data);
535: if (ret <= 0) {
536: folio_put(folio);
537: goto out;
538: }
539:
540: /* We are going to replace instruction, update ref_ctr. */
541: if (do_update_ref_ctr && !ref_ctr_updated && uprobe->ref_ctr_offset) {
542: ret = update_ref_ctr(uprobe, mm, is_register ? 1 : -1);
543: if (ret) {
544: folio_put(folio);
545: goto out;
546: }
547:
548: ref_ctr_updated = 1;
549: }
550:
551: ret = 0;
552: if (unlikely(!folio_test_anon(folio) || folio_is_zone_device(folio))) {
553: VM_WARN_ON_ONCE(is_register);
554: folio_put(folio);
555: goto out;
556: }
557:
558: if (!is_register) {
559: /*
560: * In the common case, we'll be able to zap the page when
561: * unregistering. So trigger MMU notifiers now, as we won't
562: * be able to do it under PTL.
563: */
564: mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm,
565: vaddr, vaddr + PAGE_SIZE);
566: mmu_notifier_invalidate_range_start(&range);
567: }
568:
569: ret = -EAGAIN;
570: /* Walk the page tables again, to perform the actual update. */
571: if (folio_walk_start(&fw, vma, vaddr, 0)) {
572: if (fw.page == page)
573: ret = __uprobe_write(vma, &fw, folio, insn_vaddr, insn, nbytes, is_register);
574: folio_walk_end(&fw, vma);
575: }
576:
577: if (!is_register)
578: mmu_notifier_invalidate_range_end(&range);
579:
580: folio_put(folio);
581: switch (ret) {
582: case -EFAULT:
583: gup_flags |= FOLL_WRITE | FOLL_SPLIT_PMD;
584: fallthrough;
585: case -EAGAIN:
586: goto retry;
587: default:
588: break;
589: }
590:
591: out:
592: /* Revert back reference counter if instruction update failed. */
593: if (do_update_ref_ctr && ret < 0 && ref_ctr_updated)
594: update_ref_ctr(uprobe, mm, is_register ? -1 : 1);
595:
596: /* try collapse pmd for compound page */
597: if (ret > 0)
598: collapse_pte_mapped_thp(mm, vaddr, false);
599:
600: return ret < 0 ? ret : 0;
601: }
SourceFile:kernel/events/uprobes.c]
|
| 92/2 |
2026/06/02 16:19 |
llm |
expert |
0m
Model:
Tokens:
input: 34987
output: 38
thoughts: 23
Thoughts: Here's my thought process, summarized as if I were considering the situation described:
**Analyzing the `__uprobe_write` Call**
Okay, so the trace shows `__uprobe_write` being invoked. Interesting. That's a key function in uprobe handling. Given my understanding of the system, this suggests some user-space probe is being triggered, or is at least in the process of triggering. My focus now needs to be on understanding *why* this call is happening. I need to delve into the context of `__uprobe_write`. Is it being called as expected, or is there an unexpected execution path that's led us here? What's the state of the system when this call is made? Time to dive into the source code and relevant data structures. I need to determine the cause and the effect.
|
| 93/2 |
2026/06/02 16:19 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:kernel/events/uprobes.c Name:__uprobe_write]
Results: map[SourceCode: 403: static int __uprobe_write(struct vm_area_struct *vma,
404: struct folio_walk *fw, struct folio *folio,
405: unsigned long insn_vaddr, uprobe_opcode_t *insn, int nbytes,
406: bool is_register)
407: {
408: const unsigned long vaddr = insn_vaddr & PAGE_MASK;
409: bool pmd_mappable;
410:
411: /* For now, we'll only handle PTE-mapped folios. */
412: if (fw->level != FW_LEVEL_PTE)
413: return -EFAULT;
414:
415: /*
416: * See can_follow_write_pte(): we'd actually prefer a writable PTE here,
417: * but the VMA might not be writable.
418: */
419: if (!pte_write(fw->pte)) {
420: if (!PageAnonExclusive(fw->page))
421: return -EFAULT;
422: if (unlikely(userfaultfd_pte_wp(vma, fw->pte)))
423: return -EFAULT;
424: /* SOFTDIRTY is handled via pte_mkdirty() below. */
425: }
426:
427: /*
428: * We'll temporarily unmap the page and flush the TLB, such that we can
429: * modify the page atomically.
430: */
431: flush_cache_page(vma, vaddr, pte_pfn(fw->pte));
432: fw->pte = ptep_clear_flush(vma, vaddr, fw->ptep);
433: copy_to_page(fw->page, insn_vaddr, insn, nbytes);
434:
435: /*
436: * When unregistering, we may only zap a PTE if uffd is disabled and
437: * there are no unexpected folio references ...
438: */
439: if (is_register || userfaultfd_missing(vma) ||
440: (folio_ref_count(folio) != folio_expected_ref_count(folio) + 1))
441: goto remap;
442:
443: /*
444: * ... and the mapped page is identical to the original page that
445: * would get faulted in on next access.
446: */
447: if (!orig_page_is_identical(vma, vaddr, fw->page, &pmd_mappable))
448: goto remap;
449:
450: dec_mm_counter(vma->vm_mm, MM_ANONPAGES);
451: folio_remove_rmap_pte(folio, fw->page, vma);
452: if (!folio_mapped(folio) && folio_test_swapcache(folio) &&
453: folio_trylock(folio)) {
454: folio_free_swap(folio);
455: folio_unlock(folio);
456: }
457: folio_put(folio);
458:
459: return pmd_mappable;
460: remap:
461: /*
462: * Make sure that our copy_to_page() changes become visible before the
463: * set_pte_at() write.
464: */
465: smp_wmb();
466: /* We modified the page. Make sure to mark the PTE dirty. */
467: set_pte_at(vma->vm_mm, vaddr, fw->ptep, pte_mkdirty(fw->pte));
468: return 0;
469: }
SourceFile:kernel/events/uprobes.c]
|
| 94/2 |
2026/06/02 16:19 |
llm |
expert |
0m
Model:
Tokens:
input: 36343
output: 40
thoughts: 239
Thoughts: **Debugging a Memory Accounting Discrepancy**
Okay, so I see `__uprobe_write` calls `dec_mm_counter(vma->vm_mm, MM_ANONPAGES)`. Hmm, but the bug report I'm looking at shows an issue with `MM_FILEPAGES val:1` and `MM_SWAPENTS val:32`. That discrepancy is intriguing. Let's start by seeing if there are any related syzbot issues by searching for those tags.
I wonder if I could use `grepper` to look for the memory tags in the history of `kernel/fork.c`. Nope, that doesn't seem to be the correct path. Okay, let me broaden the search and look for `MM_SWAPENTS` again, focusing on the `mm/` directory.
The key question is: are there any places where `MM_SWAPENTS` is incremented, but *not* properly decremented? If a process exits with `MM_SWAPENTS val:32` remaining, it suggests that 32 swap entries weren't accounted for correctly.
That makes me think about potential interaction with `madvise`. Let's investigate `madvise_free_pte_range` in `mm/madvise.c`. It's a prime candidate for disrupting the swap accounting.
|
| 95/2 |
2026/06/02 16:19 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/madvise.c Name:madvise_free_pte_range]
Results: map[SourceCode: 651: static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
652: unsigned long end, struct mm_walk *walk)
653:
654: {
655: const cydp_t cydp_flags = CYDP_CLEAR_YOUNG | CYDP_CLEAR_DIRTY;
656: struct mmu_gather *tlb = walk->private;
657: struct mm_struct *mm = tlb->mm;
658: struct vm_area_struct *vma = walk->vma;
659: spinlock_t *ptl;
660: pte_t *start_pte, *pte, ptent;
661: struct folio *folio;
662: int nr_swap = 0;
663: unsigned long next;
664: int nr, max_nr;
665:
666: next = pmd_addr_end(addr, end);
667: if (pmd_trans_huge(*pmd))
668: if (madvise_free_huge_pmd(tlb, vma, pmd, addr, next))
669: return 0;
670:
671: tlb_change_page_size(tlb, PAGE_SIZE);
672: start_pte = pte = pte_offset_map_lock(mm, pmd, addr, &ptl);
673: if (!start_pte)
674: return 0;
675: flush_tlb_batched_pending(mm);
676: lazy_mmu_mode_enable();
677: for (; addr != end; pte += nr, addr += PAGE_SIZE * nr) {
678: nr = 1;
679: ptent = ptep_get(pte);
680:
681: if (pte_none(ptent))
682: continue;
683: /*
684: * If the pte has swp_entry, just clear page table to
685: * prevent swap-in which is more expensive rather than
686: * (page allocation + zeroing).
687: */
688: if (!pte_present(ptent)) {
689: softleaf_t entry = softleaf_from_pte(ptent);
690:
691: if (softleaf_is_swap(entry)) {
692: max_nr = (end - addr) / PAGE_SIZE;
693: nr = swap_pte_batch(pte, max_nr, ptent);
694: nr_swap -= nr;
695: swap_put_entries_direct(entry, nr);
696: clear_not_present_full_ptes(mm, addr, pte, nr, tlb->fullmm);
697: } else if (softleaf_is_hwpoison(entry) ||
698: softleaf_is_poison_marker(entry)) {
699: pte_clear_not_present_full(mm, addr, pte, tlb->fullmm);
700: }
701: continue;
702: }
703:
704: folio = vm_normal_folio(vma, addr, ptent);
705: if (!folio || folio_is_zone_device(folio))
706: continue;
707:
708: /*
709: * If we encounter a large folio, only split it if it is not
710: * fully mapped within the range we are operating on. Otherwise
711: * leave it as is so that it can be marked as lazyfree. If we
712: * fail to split a folio, leave it in place and advance to the
713: * next pte in the range.
714: */
715: if (folio_test_large(folio)) {
716: nr = madvise_folio_pte_batch(addr, end, folio, pte, &ptent);
717: if (nr < folio_nr_pages(folio)) {
718: int err;
719:
720: if (folio_maybe_mapped_shared(folio))
721: continue;
722: if (!folio_trylock(folio))
723: continue;
724: folio_get(folio);
725: lazy_mmu_mode_disable();
726: pte_unmap_unlock(start_pte, ptl);
727: start_pte = NULL;
728: err = split_folio(folio);
729: folio_unlock(folio);
730: folio_put(folio);
731: pte = pte_offset_map_lock(mm, pmd, addr, &ptl);
732: start_pte = pte;
733: if (!start_pte)
734: break;
735: flush_tlb_batched_pending(mm);
736: lazy_mmu_mode_enable();
737: if (!err)
738: nr = 0;
739: continue;
740: }
741: }
742:
743: if (folio_test_swapcache(folio) || folio_test_dirty(folio)) {
744: if (!folio_trylock(folio))
745: continue;
746: /*
747: * If we have a large folio at this point, we know it is
748: * fully mapped so if its mapcount is the same as its
749: * number of pages, it must be exclusive.
750: */
751: if (folio_mapcount(folio) != folio_nr_pages(folio)) {
752: folio_unlock(folio);
753: continue;
754: }
755:
756: if (folio_test_swapcache(folio) &&
757: !folio_free_swap(folio)) {
758: folio_unlock(folio);
759: continue;
760: }
761:
762: folio_clear_dirty(folio);
763: folio_unlock(folio);
764: }
765:
766: if (pte_young(ptent) || pte_dirty(ptent)) {
767: clear_young_dirty_ptes(vma, addr, pte, nr, cydp_flags);
768: tlb_remove_tlb_entries(tlb, pte, nr, addr);
769: }
770: folio_mark_lazyfree(folio);
771: }
772:
773: if (nr_swap)
774: add_mm_counter(mm, MM_SWAPENTS, nr_swap);
775: if (start_pte) {
776: lazy_mmu_mode_disable();
777: pte_unmap_unlock(start_pte, ptl);
778: }
779: cond_resched();
780:
781: return 0;
782: }
SourceFile:mm/madvise.c]
|
| 96/2 |
2026/06/02 16:19 |
llm |
expert |
0m
Model:
Tokens:
input: 38988
output: 31
thoughts: 309
Thoughts: **My Analysis of `madvise_free_pte_range` and Swap Accounting**
Okay, let's break this down. My initial observation is about `madvise_free_pte_range`'s interaction with the swap accounting. I see that `nr_swap` is first *decremented* by `nr` (`nr_swap -= nr;`) and then *added* to `MM_SWAPENTS` (`add_mm_counter(mm, MM_SWAPENTS, nr_swap);`). Effectively, this means it subtracts from the swap entry count, which appears logical. We're freeing PTEs, so we're reducing the swap entries associated with them. This initial assessment feels right.
But, a potential wrinkle emerges. What if `madvise_free_pte_range` encounters a `softleaf_is_swap(entry)` that, for some reason, is actually a migration entry and not a true swap entry? However, `softleaf_is_swap` *should* correctly identify swap entries, so that seems unlikely.
Next, a question of context. What if `madvise_free_pte_range` gets called on a file mapping? A quick check confirms `madvise_free` is *supposed* to be for anonymous memory, using `MADV_FREE`. Okay, so file mappings shouldn't be a problem here.
Wait, though… `madvise_free` can be applied to shmem? Yes, the documentation indicates that `MADV_FREE` *does* work on both anonymous and shmem memory. This introduces a subtle complexity. If it *is* shmem, `softleaf_is_swap` *might* evaluate to true for shmem pages that have been swapped out.
And here's where my concern intensifies. If shmem pages *are* swapped out, how are their swap entries accounted for in the overall system? Are shmem swap entries included in `MM_SWAPENTS`? I don't *think* so, but I need to confirm. This is the crucial point. The accounting could be off. I need to check `shmem_add_to_page_cache`.
|
| 97/2 |
2026/06/02 16:19 |
tool |
grepper |
0m
Error:
Args: map[Expression:MM_SWAPENTS PathPrefix:mm/shmem.c]
Results:
|
| 98/2 |
2026/06/02 16:19 |
llm |
expert |
0m
Model:
Tokens:
input: 39819
output: 34
thoughts: 72
Thoughts: Here's my understanding, as if I were thinking it:
**Shmem Swap Accounting Clarification**
Okay, so I'm digging into how shared memory (shmem) swap entries are accounted for in the kernel. My initial thought was that it would be reflected within the generic memory management statistics, specifically `mm->rss_stat[MM_SWAPENTS]`. But wait, that's not quite right. That counter seems to be strictly dedicated to anonymous memory swap entries. So, where does shmem's swapped data get tracked?
The most logical place to look is within the shmem-specific data structures, and that would be `shmem_inode_info->swapped`. Seems pretty straightforward in hindsight!
Now, to solidify this understanding, I think I need to trace the actual swap-out process. I'll take a look at the `do_swap_page` function within `mm/memory.c`. That should show me the specific logic path that increments or updates the relevant counters during a swap-out operation. This is key to ensuring my understanding is correct and complete.
|
| 99/2 |
2026/06/02 16:19 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/memory.c Name:do_swap_page]
Results: map[SourceCode:4698: /*
4699: * We enter with non-exclusive mmap_lock (to exclude vma changes,
4700: * but allow concurrent faults), and pte mapped but not yet locked.
4701: * We return with pte unmapped and unlocked.
4702: *
4703: * We return with the mmap_lock locked or unlocked in the same cases
4704: * as does filemap_fault().
4705: */
4706: vm_fault_t do_swap_page(struct vm_fault *vmf)
4707: {
4708: struct vm_area_struct *vma = vmf->vma;
4709: struct folio *swapcache = NULL, *folio;
4710: struct page *page;
4711: struct swap_info_struct *si = NULL;
4712: rmap_t rmap_flags = RMAP_NONE;
4713: bool exclusive = false;
4714: softleaf_t entry;
4715: pte_t pte;
4716: vm_fault_t ret = 0;
4717: int nr_pages;
4718: unsigned long page_idx;
4719: unsigned long address;
4720: pte_t *ptep;
4721:
4722: if (!pte_unmap_same(vmf))
4723: goto out;
4724:
4725: entry = softleaf_from_pte(vmf->orig_pte);
4726: if (unlikely(!softleaf_is_swap(entry))) {
4727: if (softleaf_is_migration(entry)) {
4728: migration_entry_wait(vma->vm_mm, vmf->pmd,
4729: vmf->address);
4730: } else if (softleaf_is_device_exclusive(entry)) {
4731: vmf->page = softleaf_to_page(entry);
4732: ret = remove_device_exclusive_entry(vmf);
4733: } else if (softleaf_is_device_private(entry)) {
4734: if (vmf->flags & FAULT_FLAG_VMA_LOCK) {
4735: /*
4736: * migrate_to_ram is not yet ready to operate
4737: * under VMA lock.
4738: */
4739: vma_end_read(vma);
4740: ret = VM_FAULT_RETRY;
4741: goto out;
4742: }
4743:
4744: vmf->page = softleaf_to_page(entry);
4745: vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
4746: vmf->address, &vmf->ptl);
4747: if (unlikely(!vmf->pte ||
4748: !pte_same(ptep_get(vmf->pte),
4749: vmf->orig_pte)))
4750: goto unlock;
4751:
4752: /*
4753: * Get a page reference while we know the page can't be
4754: * freed.
4755: */
4756: if (trylock_page(vmf->page)) {
4757: struct dev_pagemap *pgmap;
4758:
4759: get_page(vmf->page);
4760: pte_unmap_unlock(vmf->pte, vmf->ptl);
4761: pgmap = page_pgmap(vmf->page);
4762: ret = pgmap->ops->migrate_to_ram(vmf);
4763: unlock_page(vmf->page);
4764: put_page(vmf->page);
4765: } else {
4766: pte_unmap_unlock(vmf->pte, vmf->ptl);
4767: }
4768: } else if (softleaf_is_hwpoison(entry)) {
4769: ret = VM_FAULT_HWPOISON;
4770: } else if (softleaf_is_marker(entry)) {
4771: ret = handle_pte_marker(vmf);
4772: } else {
4773: print_bad_pte(vma, vmf->address, vmf->orig_pte, NULL);
4774: ret = VM_FAULT_SIGBUS;
4775: }
4776: goto out;
4777: }
4778:
4779: /* Prevent swapoff from happening to us. */
4780: si = get_swap_device(entry);
4781: if (unlikely(!si))
4782: goto out;
4783:
4784: folio = swap_cache_get_folio(entry);
4785: if (folio)
4786: swap_update_readahead(folio, vma, vmf->address);
4787: if (!folio) {
4788: if (data_race(si->flags & SWP_SYNCHRONOUS_IO)) {
4789: folio = alloc_swap_folio(vmf);
4790: if (folio) {
4791: /*
4792: * folio is charged, so swapin can only fail due
4793: * to raced swapin and return NULL.
4794: */
4795: swapcache = swapin_folio(entry, folio);
4796: if (swapcache != folio)
4797: folio_put(folio);
4798: folio = swapcache;
4799: }
4800: } else {
4801: folio = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE, vmf);
4802: }
4803:
4804: if (!folio) {
4805: /*
4806: * Back out if somebody else faulted in this pte
4807: * while we released the pte lock.
4808: */
4809: vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
4810: vmf->address, &vmf->ptl);
4811: if (likely(vmf->pte &&
4812: pte_same(ptep_get(vmf->pte), vmf->orig_pte)))
4813: ret = VM_FAULT_OOM;
4814: goto unlock;
4815: }
4816:
4817: /* Had to read the page from swap area: Major fault */
4818: ret = VM_FAULT_MAJOR;
4819: count_vm_event(PGMAJFAULT);
4820: count_memcg_event_mm(vma->vm_mm, PGMAJFAULT);
4821: }
4822:
4823: swapcache = folio;
4824: ret |= folio_lock_or_retry(folio, vmf);
4825: if (ret & VM_FAULT_RETRY)
4826: goto out_release;
4827:
4828: page = folio_file_page(folio, swp_offset(entry));
4829: /*
4830: * Make sure folio_free_swap() or swapoff did not release the
4831: * swapcache from under us. The page pin, and pte_same test
4832: * below, are not enough to exclude that. Even if it is still
4833: * swapcache, we need to check that the page's swap has not
4834: * changed.
4835: */
4836: if (unlikely(!folio_matches_swap_entry(folio, entry)))
4837: goto out_page;
4838:
4839: if (unlikely(PageHWPoison(page))) {
4840: /*
4841: * hwpoisoned dirty swapcache pages are kept for killing
4842: * owner processes (which may be unknown at hwpoison time)
4843: */
4844: ret = VM_FAULT_HWPOISON;
4845: goto out_page;
4846: }
4847:
4848: /*
4849: * KSM sometimes has to copy on read faults, for example, if
4850: * folio->index of non-ksm folios would be nonlinear inside the
4851: * anon VMA -- the ksm flag is lost on actual swapout.
4852: */
4853: folio = ksm_might_need_to_copy(folio, vma, vmf->address);
4854: if (unlikely(!folio)) {
4855: ret = VM_FAULT_OOM;
4856: folio = swapcache;
4857: goto out_page;
4858: } else if (unlikely(folio == ERR_PTR(-EHWPOISON))) {
4859: ret = VM_FAULT_HWPOISON;
4860: folio = swapcache;
4861: goto out_page;
4862: } else if (folio != swapcache)
4863: page = folio_page(folio, 0);
4864:
4865: /*
4866: * If we want to map a page that's in the swapcache writable, we
4867: * have to detect via the refcount if we're really the exclusive
4868: * owner. Try removing the extra reference from the local LRU
4869: * caches if required.
4870: */
4871: if ((vmf->flags & FAULT_FLAG_WRITE) &&
4872: !folio_test_ksm(folio) && !folio_test_lru(folio))
4873: lru_add_drain();
4874:
4875: folio_throttle_swaprate(folio, GFP_KERNEL);
4876:
4877: /*
4878: * Back out if somebody else already faulted in this pte.
4879: */
4880: vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
4881: &vmf->ptl);
4882: if (unlikely(!vmf->pte || !pte_same(ptep_get(vmf->pte), vmf->orig_pte)))
4883: goto out_nomap;
4884:
4885: if (unlikely(!folio_test_uptodate(folio))) {
4886: ret = VM_FAULT_SIGBUS;
4887: goto out_nomap;
4888: }
4889:
4890: nr_pages = 1;
4891: page_idx = 0;
4892: address = vmf->address;
4893: ptep = vmf->pte;
4894: if (folio_test_large(folio) && folio_test_swapcache(folio)) {
4895: int nr = folio_nr_pages(folio);
4896: unsigned long idx = folio_page_idx(folio, page);
4897: unsigned long folio_start = address - idx * PAGE_SIZE;
4898: unsigned long folio_end = folio_start + nr * PAGE_SIZE;
4899: pte_t *folio_ptep;
4900: pte_t folio_pte;
4901:
4902: if (unlikely(folio_start < max(address & PMD_MASK, vma->vm_start)))
4903: goto check_folio;
4904: if (unlikely(folio_end > pmd_addr_end(address, vma->vm_end)))
4905: goto check_folio;
4906:
4907: folio_ptep = vmf->pte - idx;
4908: folio_pte = ptep_get(folio_ptep);
4909: if (!pte_same(folio_pte, pte_move_swp_offset(vmf->orig_pte, -idx)) ||
4910: swap_pte_batch(folio_ptep, nr, folio_pte) != nr)
4911: goto check_folio;
4912:
4913: page_idx = idx;
4914: address = folio_start;
4915: ptep = folio_ptep;
4916: nr_pages = nr;
4917: entry = folio->swap;
4918: page = &folio->page;
4919: }
4920:
4921: check_folio:
4922: /*
4923: * PG_anon_exclusive reuses PG_mappedtodisk for anon pages. A swap pte
4924: * must never point at an anonymous page in the swapcache that is
4925: * PG_anon_exclusive. Sanity check that this holds and especially, that
4926: * no filesystem set PG_mappedtodisk on a page in the swapcache. Sanity
4927: * check after taking the PT lock and making sure that nobody
4928: * concurrently faulted in this page and set PG_anon_exclusive.
4929: */
4930: BUG_ON(!folio_test_anon(folio) && folio_test_mappedtodisk(folio));
4931: BUG_ON(folio_test_anon(folio) && PageAnonExclusive(page));
4932:
4933: /*
4934: * If a large folio already belongs to anon mapping, then we
4935: * can just go on and map it partially.
4936: * If not, with the large swapin check above failing, the page table
4937: * have changed, so sub pages might got charged to the wrong cgroup,
4938: * or even should be shmem. So we have to free it and fallback.
4939: * Nothing should have touched it, both anon and shmem checks if a
4940: * large folio is fully appliable before use.
4941: *
4942: * This will be removed once we unify folio allocation in the swap cache
4943: * layer, where allocation of a folio stabilizes the swap entries.
4944: */
4945: if (!folio_test_anon(folio) && folio_test_large(folio) &&
4946: nr_pages != folio_nr_pages(folio)) {
4947: if (!WARN_ON_ONCE(folio_test_dirty(folio)))
4948: swap_cache_del_folio(folio);
4949: goto out_nomap;
4950: }
4951:
4952: /*
4953: * Check under PT lock (to protect against concurrent fork() sharing
4954: * the swap entry concurrently) for certainly exclusive pages.
4955: */
4956: if (!folio_test_ksm(folio)) {
4957: /*
4958: * The can_swapin_thp check above ensures all PTE have
4959: * same exclusiveness. Checking just one PTE is fine.
4960: */
4961: exclusive = pte_swp_exclusive(vmf->orig_pte);
4962: if (exclusive)
4963: check_swap_exclusive(folio, entry, nr_pages);
4964: if (folio != swapcache) {
4965: /*
4966: * We have a fresh page that is not exposed to the
4967: * swapcache -> certainly exclusive.
4968: */
4969: exclusive = true;
4970: } else if (exclusive && folio_test_writeback(folio) &&
4971: data_race(si->flags & SWP_STABLE_WRITES)) {
4972: /*
4973: * This is tricky: not all swap backends support
4974: * concurrent page modifications while under writeback.
4975: *
4976: * So if we stumble over such a page in the swapcache
4977: * we must not set the page exclusive, otherwise we can
4978: * map it writable without further checks and modify it
4979: * while still under writeback.
4980: *
4981: * For these problematic swap backends, simply drop the
4982: * exclusive marker: this is perfectly fine as we start
4983: * writeback only if we fully unmapped the page and
4984: * there are no unexpected references on the page after
4985: * unmapping succeeded. After fully unmapped, no
4986: * further GUP references (FOLL_GET and FOLL_PIN) can
4987: * appear, so dropping the exclusive marker and mapping
4988: * it only R/O is fine.
4989: */
4990: exclusive = false;
4991: }
4992: }
4993:
4994: /*
4995: * Some architectures may have to restore extra metadata to the page
4996: * when reading from swap. This metadata may be indexed by swap entry
4997: * so this must be called before folio_put_swap().
4998: */
4999: arch_swap_restore(folio_swap(entry, folio), folio);
5000:
5001: add_mm_counter(vma->vm_mm, MM_ANONPAGES, nr_pages);
5002: add_mm_counter(vma->vm_mm, MM_SWAPENTS, -nr_pages);
5003: pte = mk_pte(page, vma->vm_page_prot);
5004: if (pte_swp_soft_dirty(vmf->orig_pte))
5005: pte = pte_mksoft_dirty(pte);
5006: if (pte_swp_uffd_wp(vmf->orig_pte))
5007: pte = pte_mkuffd_wp(pte);
5008:
5009: /*
5010: * Same logic as in do_wp_page(); however, optimize for pages that are
5011: * certainly not shared either because we just allocated them without
5012: * exposing them to the swapcache or because the swap entry indicates
5013: * exclusivity.
5014: */
5015: if (!folio_test_ksm(folio) &&
5016: (exclusive || folio_ref_count(folio) == 1)) {
5017: if ((vma->vm_flags & VM_WRITE) && !userfaultfd_pte_wp(vma, pte) &&
5018: !pte_needs_soft_dirty_wp(vma, pte)) {
5019: pte = pte_mkwrite(pte, vma);
5020: if (vmf->flags & FAULT_FLAG_WRITE) {
5021: pte = pte_mkdirty(pte);
5022: vmf->flags &= ~FAULT_FLAG_WRITE;
5023: }
5024: }
5025: rmap_flags |= RMAP_EXCLUSIVE;
5026: }
5027: folio_ref_add(folio, nr_pages - 1);
5028: flush_icache_pages(vma, page, nr_pages);
5029: vmf->orig_pte = pte_advance_pfn(pte, page_idx);
5030:
5031: /* ksm created a completely new copy */
5032: if (unlikely(folio != swapcache)) {
5033: folio_add_new_anon_rmap(folio, vma, address, RMAP_EXCLUSIVE);
5034: folio_add_lru_vma(folio, vma);
5035: folio_put_swap(swapcache, NULL);
5036: } else if (!folio_test_anon(folio)) {
5037: /*
5038: * We currently only expect !anon folios that are fully
5039: * mappable. See the comment after can_swapin_thp above.
5040: */
5041: VM_WARN_ON_ONCE_FOLIO(folio_nr_pages(folio) != nr_pages, folio);
5042: VM_WARN_ON_ONCE_FOLIO(folio_mapped(folio), folio);
5043: folio_add_new_anon_rmap(folio, vma, address, rmap_flags);
5044: folio_put_swap(folio, NULL);
5045: } else {
5046: VM_WARN_ON_ONCE(nr_pages != 1 && nr_pages != folio_nr_pages(folio));
5047: folio_add_anon_rmap_ptes(folio, page, nr_pages, vma, address,
5048: rmap_flags);
5049: folio_put_swap(folio, nr_pages == 1 ? page : NULL);
5050: }
5051:
5052: VM_BUG_ON(!folio_test_anon(folio) ||
5053: (pte_write(pte) && !PageAnonExclusive(page)));
5054: set_ptes(vma->vm_mm, address, ptep, pte, nr_pages);
5055: arch_do_swap_page_nr(vma->vm_mm, vma, address,
5056: pte, pte, nr_pages);
5057:
5058: /*
5059: * Remove the swap entry and conditionally try to free up the swapcache.
5060: * Do it after mapping, so raced page faults will likely see the folio
5061: * in swap cache and wait on the folio lock.
5062: */
5063: if (should_try_to_free_swap(si, folio, vma, nr_pages, vmf->flags))
5064: folio_free_swap(folio);
5065:
5066: folio_unlock(folio);
5067: if (unlikely(folio != swapcache)) {
5068: /*
5069: * Hold the lock to avoid the swap entry to be reused
5070: * until we take the PT lock for the pte_same() check
5071: * (to avoid false positives from pte_same). For
5072: * further safety release the lock after the folio_put_swap
5073: * so that the swap count won't change under a
5074: * parallel locked swapcache.
5075: */
5076: folio_unlock(swapcache);
5077: folio_put(swapcache);
5078: }
5079:
5080: if (vmf->flags & FAULT_FLAG_WRITE) {
5081: ret |= do_wp_page(vmf);
5082: if (ret & VM_FAULT_ERROR)
5083: ret &= VM_FAULT_ERROR;
5084: goto out;
5085: }
5086:
5087: /* No need to invalidate - it was non-present before */
5088: update_mmu_cache_range(vmf, vma, address, ptep, nr_pages);
5089: unlock:
5090: if (vmf->pte)
5091: pte_unmap_unlock(vmf->pte, vmf->ptl);
5092: out:
5093: if (si)
5094: put_swap_device(si);
5095: return ret;
5096: out_nomap:
5097: if (vmf->pte)
5098: pte_unmap_unlock(vmf->pte, vmf->ptl);
5099: out_page:
5100: if (folio_test_swapcache(folio))
5101: folio_free_swap(folio);
5102: folio_unlock(folio);
5103: out_release:
5104: folio_put(folio);
5105: if (folio != swapcache) {
5106: folio_unlock(swapcache);
5107: folio_put(swapcache);
5108: }
5109: if (si)
5110: put_swap_device(si);
5111: return ret;
5112: }
SourceFile:mm/memory.c]
|
| 100/2 |
2026/06/02 16:19 |
llm |
expert |
0m
Model:
Tokens:
input: 46928
output: 25
thoughts: 1272
Thoughts: **Analyzing Potential Memory Accounting Issues in `do_swap_page` and Possible Uprobe Connections**
Okay, let's break this down. My initial thought process revolves around `do_swap_page` and potential counter inconsistencies, but I need to refine my understanding. I see the code adds and subtracts from memory counters (`MM_ANONPAGES`, `MM_SWAPENTS`) early on, before `set_ptes` and the potentially failing `do_wp_page`. Initially, it seems robust because if `do_swap_page` fails, the counters are valid at the point of failure. The crucial point is that `do_swap_page` happens before `do_wp_page`, so the counters should be accurate.
My next line of thinking involved the possibility of shmem pages somehow slipping into `do_swap_page`, but the control flow seems to prevent this. `do_swap_page` is specifically for anonymous memory; shmem has its dedicated handlers. However, the logic with `folio_test_anon(folio)` caught my attention. It can be false in `do_swap_page`, which confused me initially, because it appeared to be related to shmem. However, the comment clarified the situation to be about newly allocated pages. The code then calls `folio_add_new_anon_rmap` which *marks* the page as anonymous.
Now, let's pivot to the bug report with bad RSS counters: `MM_FILEPAGES` is 1 and `MM_SWAPENTS` is 32. I'm wondering if this is related to a previously identified uprobe issue. My first thought was the previous uprobe bug report, but that had `MM_FILEPAGES val:-1`. Then I remembered the `uprobe` code decreases `MM_ANONPAGES`. It seems my initial assumption was off and I was looking at the wrong counter type.
The focus shifts to understanding the RSS counter issue in the `uprobe` context. I need to recall the details of the previous uprobe bug (`MM_FILEPAGES val:-1`). Now I'm recalling something about zero pages from the commit message. The commit message for `bddf10d26e6e` says zero pages are set to the PTE without increasing the RSS count. Then the operation in `uprobe_write_opcode()->__replace_page()` decreases the RSS count. I believe this part of the code was renamed to `__uprobe_write`. Therefore the counter issue might relate to that. The bug might be about writing to a PTE pointing to a zero page without an increase in RSS counter.
|
| 101/2 |
2026/06/02 16:19 |
tool |
git-show |
0m
Args:
Results: map[Output:commit bddf10d26e6e5114e7415a0e442ec6f51a559468
Author: Tong Tiangen <tongtiangen@huawei.com>
Date: Mon Feb 24 11:11:49 2025 +0800
uprobes: Reject the shared zeropage in uprobe_write_opcode()
We triggered the following crash in syzkaller tests:
BUG: Bad page state in process syz.7.38 pfn:1eff3
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x1eff3
flags: 0x3fffff00004004(referenced|reserved|node=0|zone=1|lastcpupid=0x1fffff)
raw: 003fffff00004004 ffffe6c6c07bfcc8 ffffe6c6c07bfcc8 0000000000000000
raw: 0000000000000000 0000000000000000 00000000fffffffe 0000000000000000
page dumped because: PAGE_FLAGS_CHECK_AT_FREE flag(s) set
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1.1 04/01/2014
Call Trace:
<TASK>
dump_stack_lvl+0x32/0x50
bad_page+0x69/0xf0
free_unref_page_prepare+0x401/0x500
free_unref_page+0x6d/0x1b0
uprobe_write_opcode+0x460/0x8e0
install_breakpoint.part.0+0x51/0x80
register_for_each_vma+0x1d9/0x2b0
__uprobe_register+0x245/0x300
bpf_uprobe_multi_link_attach+0x29b/0x4f0
link_create+0x1e2/0x280
__sys_bpf+0x75f/0xac0
__x64_sys_bpf+0x1a/0x30
do_syscall_64+0x56/0x100
entry_SYSCALL_64_after_hwframe+0x78/0xe2
BUG: Bad rss-counter state mm:00000000452453e0 type:MM_FILEPAGES val:-1
The following syzkaller test case can be used to reproduce:
r2 = creat(&(0x7f0000000000)='./file0\x00', 0x8)
write$nbd(r2, &(0x7f0000000580)=ANY=[], 0x10)
r4 = openat(0xffffffffffffff9c, &(0x7f0000000040)='./file0\x00', 0x42, 0x0)
mmap$IORING_OFF_SQ_RING(&(0x7f0000ffd000/0x3000)=nil, 0x3000, 0x0, 0x12, r4, 0x0)
r5 = userfaultfd(0x80801)
ioctl$UFFDIO_API(r5, 0xc018aa3f, &(0x7f0000000040)={0xaa, 0x20})
r6 = userfaultfd(0x80801)
ioctl$UFFDIO_API(r6, 0xc018aa3f, &(0x7f0000000140))
ioctl$UFFDIO_REGISTER(r6, 0xc020aa00, &(0x7f0000000100)={{&(0x7f0000ffc000/0x4000)=nil, 0x4000}, 0x2})
ioctl$UFFDIO_ZEROPAGE(r5, 0xc020aa04, &(0x7f0000000000)={{&(0x7f0000ffd000/0x1000)=nil, 0x1000}})
r7 = bpf$PROG_LOAD(0x5, &(0x7f0000000140)={0x2, 0x3, &(0x7f0000000200)=ANY=[@ANYBLOB="1800000000120000000000000000000095"], &(0x7f0000000000)='GPL\x00', 0x7, 0x0, 0x0, 0x0, 0x0, '\x00', 0x0, @fallback=0x30, 0xffffffffffffffff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, @void, @value}, 0x94)
bpf$BPF_LINK_CREATE_XDP(0x1c, &(0x7f0000000040)={r7, 0x0, 0x30, 0x1e, @val=@uprobe_multi={&(0x7f0000000080)='./file0\x00', &(0x7f0000000100)=[0x2], 0x0, 0x0, 0x1}}, 0x40)
The cause is that zero pfn is set to the PTE without increasing the RSS
count in mfill_atomic_pte_zeropage() and the refcount of zero folio does
not increase accordingly. Then, the operation on the same pfn is performed
in uprobe_write_opcode()->__replace_page() to unconditional decrease the
RSS count and old_folio's refcount.
Therefore, two bugs are introduced:
1. The RSS count is incorrect, when process exit, the check_mm() report
error "Bad rss-count".
2. The reserved folio (zero folio) is freed when folio->refcount is zero,
then free_pages_prepare->free_page_is_bad() report error
"Bad page state".
There is more, the following warning could also theoretically be triggered:
__replace_page()
-> ...
-> folio_remove_rmap_pte()
-> VM_WARN_ON_FOLIO(is_zero_folio(folio), folio)
Considering that uprobe hit on the zero folio is a very rare case, just
reject zero old folio immediately after get_user_page_vma_remote().
[ mingo: Cleaned up the changelog ]
Fixes: 7396fa818d62 ("uprobes/core: Make background page replacement logic account for rss_stat counters")
Fixes: 2b1444983508 ("uprobes, mm, x86: Add the ability to install and remove uprobes breakpoints")
Signed-off-by: Tong Tiangen <tongtiangen@huawei.com>
Signed-off-by: Ingo Molnar <mingo@kernel.org>
Reviewed-by: David Hildenbrand <david@redhat.com>
Reviewed-by: Oleg Nesterov <oleg@redhat.com>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Link: https://lore.kernel.org/r/20250224031149.1598949-1-tongtiangen@huawei.com
diff --git a/kernel/events/uprobes.c b/kernel/events/uprobes.c
index bf2a87a0a378..af53fbd2d12c 100644
--- a/kernel/events/uprobes.c
+++ b/kernel/events/uprobes.c
@@ -495,6 +495,11 @@ int uprobe_write_opcode(struct arch_uprobe *auprobe, struct mm_struct *mm,
if (ret <= 0)
goto put_old;
+ if (is_zero_page(old_page)) {
+ ret = -EINVAL;
+ goto put_old;
+ }
+
if (WARN(!is_register && PageCompound(old_page),
"uprobe unregister should never work on compound page\n")) {
ret = -EINVAL;
]
|
| 102/2 |
2026/06/02 16:19 |
llm |
expert |
0m
Model:
Tokens:
input: 51168
output: 41
thoughts: 149
Thoughts: **Analyzing a Zero Page Vulnerability**
Okay, so this commit, `bddf10d26e6e`, highlights a critical issue. It's saying that `mfill_atomic_pte_zeropage()` is setting a zero PFN to the Page Table Entry (PTE), but crucially *not* incrementing the Resident Set Size (RSS) count. And, compounding this, the zero folio's refcount also isn't being increased. That's a huge red flag. This means we are undercounting.
Then, the problem gets worse: apparently, subsequent operations on the *same* PFN, specifically within `uprobe_write_opcode()->__replace_page()`, are unconditionally *decreasing* the RSS count and the refcount of `old_folio`. Without the RSS count increment, this seems like it could lead to some real memory management headaches, especially if it leads to use-after-free or a related concurrency issue. It doesn't seem like the RSS count can be a reliable indicator of usage in this situation.
My immediate reaction is to go back and verify the behavior of `mfill_atomic_pte_zeropage()` because that's the starting point. I need to confirm whether it truly lacks the RSS count increase. Time to delve into `mm/userfaultfd.c` and check that function's implementation. I suspect there will be an issue in that function.
|
| 103/2 |
2026/06/02 16:20 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/userfaultfd.c Name:mfill_atomic_pte_zeropage]
Results: map[SourceCode: 349: static int mfill_atomic_pte_zeropage(pmd_t *dst_pmd,
350: struct vm_area_struct *dst_vma,
351: unsigned long dst_addr)
352: {
353: pte_t _dst_pte, *dst_pte;
354: spinlock_t *ptl;
355: int ret;
356:
357: if (mm_forbids_zeropage(dst_vma->vm_mm))
358: return mfill_atomic_pte_zeroed_folio(dst_pmd, dst_vma, dst_addr);
359:
360: _dst_pte = pte_mkspecial(pfn_pte(my_zero_pfn(dst_addr),
361: dst_vma->vm_page_prot));
362: ret = -EAGAIN;
363: dst_pte = pte_offset_map_lock(dst_vma->vm_mm, dst_pmd, dst_addr, &ptl);
364: if (!dst_pte)
365: goto out;
366: if (mfill_file_over_size(dst_vma, dst_addr)) {
367: ret = -EFAULT;
368: goto out_unlock;
369: }
370: ret = -EEXIST;
371: if (!pte_none(ptep_get(dst_pte)))
372: goto out_unlock;
373: set_pte_at(dst_vma->vm_mm, dst_addr, dst_pte, _dst_pte);
374: /* No need to invalidate - it was non-present before */
375: update_mmu_cache(dst_vma, dst_addr, dst_pte);
376: ret = 0;
377: out_unlock:
378: pte_unmap_unlock(dst_pte, ptl);
379: out:
380: return ret;
381: }
SourceFile:mm/userfaultfd.c]
|
| 104/2 |
2026/06/02 16:20 |
llm |
expert |
0m
Model:
Tokens:
input: 52276
output: 33
thoughts: 441
Thoughts: **Analyzing a Potential Memory Accounting Discrepancy in Uprobes**
Okay, so I'm looking at this `mfill_atomic_pte_zeropage` function, and it maps the zero page without touching any RSS counters. That's fine; the zero page isn't considered in RSS, so that's expected behavior. Then we have `uprobe_write_opcode`, which calls `__uprobe_write`, which, if I recall correctly, used to be called `__replace_page`.
Now, `__uprobe_write` does this:
```c
dec_mm_counter(vma->vm_mm, MM_ANONPAGES);
folio_remove_rmap_pte(folio, fw->page, vma);
```
Wait a second. It *decreases* `MM_ANONPAGES`? But the bug report in `bddf10d26e6e` mentioned `MM_FILEPAGES val:-1`. Why `MM_FILEPAGES`? Aha! It's because `uprobe_write_opcode` is called on a *file-backed* VMA.
So, if it's a file-backed VMA, then `__uprobe_write` should not decrease `MM_ANONPAGES`, since that counter is for anonymous pages. This seems wrong.
Let's dig deeper and check how `__replace_page` – that's `__uprobe_write`'s predecessor – behaved in older kernels. I need to look at `kernel/events/uprobes.c` and see how it handled the memory accounting before the name change. Or, even better, let me check the current `__uprobe_write` in the current tree.
Okay, in the current tree, it *still* does `dec_mm_counter(vma->vm_mm, MM_ANONPAGES)`.
Now, if it's a file-backed VMA, the zero page is mapped, but is it accounted as `MM_FILEPAGES`? No, the zero page isn't accounted for at all, and so there should be no reason to modify the file pages counter! This is clearly a bug.
If the older `__replace_page` did decrease `MM_FILEPAGES`, it *must* have used `mm_counter_file(folio)` or something similar to adjust the correct counter. I need to check the code from before the rename to confirm this.
|
| 105/2 |
2026/06/02 16:20 |
tool |
grepper |
0m
Args: map[Expression:__replace_page PathPrefix:kernel/events/uprobes.c]
Results: map[Output:kernel/events/uprobes.c=1151=static int install_breakpoint(struct uprobe *uprobe, struct vm_area_struct *vma,
--
kernel/events/uprobes.c-1163- * set MMF_HAS_UPROBES in advance for uprobe_pre_sstep_notifier(),
kernel/events/uprobes.c:1164: * the task can hit this breakpoint right after __replace_page().
kernel/events/uprobes.c-1165- */
]
|
| 106/2 |
2026/06/02 16:20 |
llm |
expert |
0m
Model:
Tokens:
input: 53433
output: 36
thoughts: 25
Thoughts: Okay, here's my take on those thoughts, framed as an expert's internal monologue:
**Investigating `__replace_page` in the Uprobes Context**
Alright, time to get to the bottom of this. My gut feeling is telling me something's off, probably in the way uprobes are handling page replacement. Let's start with a solid approach: let's go straight to the source. I'm going to run a `git log` specifically looking for references to `__replace_page` within `kernel/events/uprobes.c`. That function seems like a key piece of the puzzle, and checking the commit history there should give me a quick understanding of its evolution and how it's been used alongside uprobes. This will tell me how it was introduced, if any recent changes were made, and maybe reveal related issues or fixes. It’s a good starting point to build a mental map of what's likely happening. This targeted search is way more efficient than sifting through the whole codebase initially. Let's see what the log tells me.
|
| 107/2 |
2026/06/02 16:20 |
tool |
git-log |
0m
Args: map[CodeRegexp:__replace_page PathPrefix:kernel/events/uprobes.c]
Results: map[Output:6e3092d788be kernel/events/uprobes: uprobe_write_opcode() rewrite
4dca82d14174 uprobes: use pagesize-aligned virtual address when replacing pages
f8ac4ec9c064 uprobes: Introduce MMF_HAS_UPROBES
c517ee744b96 uprobes: __replace_page() should not use page_address_in_vma()
a5f4374a9610 uprobes: Move to kernel/events/
]
|
| 108/2 |
2026/06/02 16:20 |
llm |
expert |
0m
Model:
Tokens:
input: 53867
output: 26
thoughts: 33
Thoughts: Okay, let's dive into this. Here's my take on those thoughts, as if they're my own:
**Analyzing that Kernel Uprobe Change**
Alright, time to get to it. First things first, let's take a look at this commit: `6e3092d788be kernel/events/uprobes: uprobe_write_opcode() rewrite`. I need to understand what this new implementation of `uprobe_write_opcode()` is all about. This function is critical for how uprobes work, specifically how they inject instrumentation into the target code. A rewrite suggests there were potential issues with the previous version: perhaps performance bottlenecks, race conditions, or maybe even subtle bugs. Knowing the old code, and the rationale behind the update will be key to understanding the full implications, especially if this commit is related to a problem or new feature. I'll need to dig into the code and the commit messages carefully. This one deserves a deeper look to see how it affects overall stability and performance.
|
| 109/2 |
2026/06/02 16:20 |
tool |
git-show |
0m
Args:
Results: map[Output:commit 6e3092d788be1de0aac56b81fc17551c76645cdf
Author: David Hildenbrand <david@kernel.org>
Date: Fri Mar 21 12:37:13 2025 +0100
kernel/events/uprobes: uprobe_write_opcode() rewrite
uprobe_write_opcode() does some pretty low-level things that really, it
shouldn't be doing: for example, manually breaking COW by allocating
anonymous folios and replacing mapped pages.
Further, it does seem to do some shaky things: for example, writing to
possible COW-shared anonymous pages or zapping anonymous pages that might
be pinned. We're also not taking care of uffd, uffd-wp, softdirty ...
although rather corner cases here. Let's just get it right like ordinary
ptrace writes would.
Let's rewrite the code, leaving COW-breaking to core-MM, triggered by
FOLL_FORCE|FOLL_WRITE (note that the code was already using FOLL_FORCE).
We'll use GUP to lookup/faultin the page and break COW if required. Then,
we'll walk the page tables using a folio_walk to perform our page
modification atomically by temporarily unmap the PTE + flushing the TLB.
Likely, we could avoid the temporary unmap in case we can just atomically
write the instruction, but that will be a separate project.
Unfortunately, we still have to implement the zapping logic manually,
because we only want to zap in specific circumstances (e.g., page content
identical).
Note that we can now handle large folios (compound pages) and the shared
zeropage just fine, so drop these checks.
Link: https://lkml.kernel.org/r/20250321113713.204682-4-david@redhat.com
Signed-off-by: David Hildenbrand <david@redhat.com>
Acked-by: Oleg Nesterov <oleg@redhat.com>
Acked-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Cc: Adrian Hunter <adrian.hunter@intel.com>
Cc: Alexander Shishkin <alexander.shishkin@linux.intel.com>
Cc: Andrii Nakryiko <andrii.nakryiko@gmail.com>
Cc: Arnaldo Carvalho de Melo <acme@kernel.org>
Cc: Ian Rogers <irogers@google.com>
Cc: Ingo Molnar <mingo@redhat.com>
Cc: Jiri Olsa <olsajiri@gmail.com>
Cc: Kan Liang <kan.liang@linux.intel.com>
Cc: Mark Rutland <mark.rutland@arm.com>
Cc: "Masami Hiramatsu (Google)" <mhiramat@kernel.org>
Cc: Matthew Wilcox (Oracle) <willy@infradead.org>
Cc: Namhyung kim <namhyung@kernel.org>
Cc: Russel King <linux@armlinux.org.uk>
Cc: tongtiangen <tongtiangen@huawei.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
diff --git a/kernel/events/uprobes.c b/kernel/events/uprobes.c
index c33d710e8db7..4c965ba77f9f 100644
--- a/kernel/events/uprobes.c
+++ b/kernel/events/uprobes.c
@@ -29,6 +29,7 @@
#include <linux/workqueue.h>
#include <linux/srcu.h>
#include <linux/oom.h> /* check_stable_address_space */
+#include <linux/pagewalk.h>
#include <linux/uprobes.h>
@@ -151,91 +152,6 @@ static loff_t vaddr_to_offset(struct vm_area_struct *vma, unsigned long vaddr)
return ((loff_t)vma->vm_pgoff << PAGE_SHIFT) + (vaddr - vma->vm_start);
}
-/**
- * __replace_page - replace page in vma by new page.
- * based on replace_page in mm/ksm.c
- *
- * @vma: vma that holds the pte pointing to page
- * @addr: address the old @page is mapped at
- * @old_page: the page we are replacing by new_page
- * @new_page: the modified page we replace page by
- *
- * If @new_page is NULL, only unmap @old_page.
- *
- * Returns 0 on success, negative error code otherwise.
- */
-static int __replace_page(struct vm_area_struct *vma, unsigned long addr,
- struct page *old_page, struct page *new_page)
-{
- struct folio *old_folio = page_folio(old_page);
- struct folio *new_folio;
- struct mm_struct *mm = vma->vm_mm;
- DEFINE_FOLIO_VMA_WALK(pvmw, old_folio, vma, addr, 0);
- int err;
- struct mmu_notifier_range range;
- pte_t pte;
-
- mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm, addr,
- addr + PAGE_SIZE);
-
- if (new_page) {
- new_folio = page_folio(new_page);
- err = mem_cgroup_charge(new_folio, vma->vm_mm, GFP_KERNEL);
- if (err)
- return err;
- }
-
- /* For folio_free_swap() below */
- folio_lock(old_folio);
-
- mmu_notifier_invalidate_range_start(&range);
- err = -EAGAIN;
- if (!page_vma_mapped_walk(&pvmw))
- goto unlock;
- VM_BUG_ON_PAGE(addr != pvmw.address, old_page);
- pte = ptep_get(pvmw.pte);
-
- /*
- * Handle PFN swap PTES, such as device-exclusive ones, that actually
- * map pages: simply trigger GUP again to fix it up.
- */
- if (unlikely(!pte_present(pte))) {
- page_vma_mapped_walk_done(&pvmw);
- goto unlock;
- }
-
- if (new_page) {
- folio_get(new_folio);
- folio_add_new_anon_rmap(new_folio, vma, addr, RMAP_EXCLUSIVE);
- folio_add_lru_vma(new_folio, vma);
- } else
- /* no new page, just dec_mm_counter for old_page */
- dec_mm_counter(mm, MM_ANONPAGES);
-
- if (!folio_test_anon(old_folio)) {
- dec_mm_counter(mm, mm_counter_file(old_folio));
- inc_mm_counter(mm, MM_ANONPAGES);
- }
-
- flush_cache_page(vma, addr, pte_pfn(pte));
- ptep_clear_flush(vma, addr, pvmw.pte);
- if (new_page)
- set_pte_at(mm, addr, pvmw.pte,
- mk_pte(new_page, vma->vm_page_prot));
-
- folio_remove_rmap_pte(old_folio, old_page, vma);
- if (!folio_mapped(old_folio))
- folio_free_swap(old_folio);
- page_vma_mapped_walk_done(&pvmw);
- folio_put(old_folio);
-
- err = 0;
- unlock:
- mmu_notifier_invalidate_range_end(&range);
- folio_unlock(old_folio);
- return err;
-}
-
/**
* is_swbp_insn - check if instruction is breakpoint instruction.
* @insn: instruction to be checked.
@@ -463,6 +379,95 @@ static int update_ref_ctr(struct uprobe *uprobe, struct mm_struct *mm,
return ret;
}
+static bool orig_page_is_identical(struct vm_area_struct *vma,
+ unsigned long vaddr, struct page *page, bool *pmd_mappable)
+{
+ const pgoff_t index = vaddr_to_offset(vma, vaddr) >> PAGE_SHIFT;
+ struct folio *orig_folio = filemap_get_folio(vma->vm_file->f_mapping,
+ index);
+ struct page *orig_page;
+ bool identical;
+
+ if (IS_ERR(orig_folio))
+ return false;
+ orig_page = folio_file_page(orig_folio, index);
+
+ *pmd_mappable = folio_test_pmd_mappable(orig_folio);
+ identical = folio_test_uptodate(orig_folio) &&
+ pages_identical(page, orig_page);
+ folio_put(orig_folio);
+ return identical;
+}
+
+static int __uprobe_write_opcode(struct vm_area_struct *vma,
+ struct folio_walk *fw, struct folio *folio,
+ unsigned long opcode_vaddr, uprobe_opcode_t opcode)
+{
+ const unsigned long vaddr = opcode_vaddr & PAGE_MASK;
+ const bool is_register = !!is_swbp_insn(&opcode);
+ bool pmd_mappable;
+
+ /* For now, we'll only handle PTE-mapped folios. */
+ if (fw->level != FW_LEVEL_PTE)
+ return -EFAULT;
+
+ /*
+ * See can_follow_write_pte(): we'd actually prefer a writable PTE here,
+ * but the VMA might not be writable.
+ */
+ if (!pte_write(fw->pte)) {
+ if (!PageAnonExclusive(fw->page))
+ return -EFAULT;
+ if (unlikely(userfaultfd_pte_wp(vma, fw->pte)))
+ return -EFAULT;
+ /* SOFTDIRTY is handled via pte_mkdirty() below. */
+ }
+
+ /*
+ * We'll temporarily unmap the page and flush the TLB, such that we can
+ * modify the page atomically.
+ */
+ flush_cache_page(vma, vaddr, pte_pfn(fw->pte));
+ fw->pte = ptep_clear_flush(vma, vaddr, fw->ptep);
+ copy_to_page(fw->page, opcode_vaddr, &opcode, UPROBE_SWBP_INSN_SIZE);
+
+ /*
+ * When unregistering, we may only zap a PTE if uffd is disabled and
+ * there are no unexpected folio references ...
+ */
+ if (is_register || userfaultfd_missing(vma) ||
+ (folio_ref_count(folio) != folio_mapcount(folio) + 1 +
+ folio_test_swapcache(folio) * folio_nr_pages(folio)))
+ goto remap;
+
+ /*
+ * ... and the mapped page is identical to the original page that
+ * would get faulted in on next access.
+ */
+ if (!orig_page_is_identical(vma, vaddr, fw->page, &pmd_mappable))
+ goto remap;
+
+ dec_mm_counter(vma->vm_mm, MM_ANONPAGES);
+ folio_remove_rmap_pte(folio, fw->page, vma);
+ if (!folio_mapped(folio) && folio_test_swapcache(folio) &&
+ folio_trylock(folio)) {
+ folio_free_swap(folio);
+ folio_unlock(folio);
+ }
+ folio_put(folio);
+
+ return pmd_mappable;
+remap:
+ /*
+ * Make sure that our copy_to_page() changes become visible before the
+ * set_pte_at() write.
+ */
+ smp_wmb();
+ /* We modified the page. Make sure to mark the PTE dirty. */
+ set_pte_at(vma->vm_mm, vaddr, fw->ptep, pte_mkdirty(fw->pte));
+ return 0;
+}
+
/*
* NOTE:
* Expect the breakpoint instruction to be the smallest size instruction for
@@ -475,116 +480,115 @@ static int update_ref_ctr(struct uprobe *uprobe, struct mm_struct *mm,
* uprobe_write_opcode - write the opcode at a given virtual address.
* @auprobe: arch specific probepoint information.
* @vma: the probed virtual memory area.
- * @vaddr: the virtual address to store the opcode.
- * @opcode: opcode to be written at @vaddr.
+ * @opcode_vaddr: the virtual address to store the opcode.
+ * @opcode: opcode to be written at @opcode_vaddr.
*
* Called with mm->mmap_lock held for read or write.
* Return 0 (success) or a negative errno.
*/
int uprobe_write_opcode(struct arch_uprobe *auprobe, struct vm_area_struct *vma,
- unsigned long vaddr, uprobe_opcode_t opcode)
+ const unsigned long opcode_vaddr, uprobe_opcode_t opcode)
{
+ const unsigned long vaddr = opcode_vaddr & PAGE_MASK;
struct mm_struct *mm = vma->vm_mm;
struct uprobe *uprobe;
- struct page *old_page, *new_page;
int ret, is_register, ref_ctr_updated = 0;
- bool orig_page_huge = false;
unsigned int gup_flags = FOLL_FORCE;
+ struct mmu_notifier_range range;
+ struct folio_walk fw;
+ struct folio *folio;
+ struct page *page;
is_register = is_swbp_insn(&opcode);
uprobe = container_of(auprobe, struct uprobe, arch);
-retry:
+ if (WARN_ON_ONCE(!is_cow_mapping(vma->vm_flags)))
+ return -EINVAL;
+
+ /*
+ * When registering, we have to break COW to get an exclusive anonymous
+ * page that we can safely modify. Use FOLL_WRITE to trigger a write
+ * fault if required. When unregistering, we might be lucky and the
+ * anon page is already gone. So defer write faults until really
+ * required. Use FOLL_SPLIT_PMD, because __uprobe_write_opcode()
+ * cannot deal with PMDs yet.
+ */
if (is_register)
- gup_flags |= FOLL_SPLIT_PMD;
- /* Read the page with vaddr into memory */
- ret = get_user_pages_remote(mm, vaddr, 1, gup_flags, &old_page, NULL);
- if (ret != 1)
- return ret;
+ gup_flags |= FOLL_WRITE | FOLL_SPLIT_PMD;
- ret = verify_opcode(old_page, vaddr, &opcode);
+retry:
+ ret = get_user_pages_remote(mm, vaddr, 1, gup_flags, &page, NULL);
if (ret <= 0)
- goto put_old;
-
- if (is_zero_page(old_page)) {
- ret = -EINVAL;
- goto put_old;
- }
+ goto out;
+ folio = page_folio(page);
- if (WARN(!is_register && PageCompound(old_page),
- "uprobe unregister should never work on compound page\n")) {
- ret = -EINVAL;
- goto put_old;
+ ret = verify_opcode(page, opcode_vaddr, &opcode);
+ if (ret <= 0) {
+ folio_put(folio);
+ goto out;
}
/* We are going to replace instruction, update ref_ctr. */
if (!ref_ctr_updated && uprobe->ref_ctr_offset) {
ret = update_ref_ctr(uprobe, mm, is_register ? 1 : -1);
- if (ret)
- goto put_old;
+ if (ret) {
+ folio_put(folio);
+ goto out;
+ }
ref_ctr_updated = 1;
}
ret = 0;
- if (!is_register && !PageAnon(old_page))
- goto put_old;
-
- ret = anon_vma_prepare(vma);
- if (ret)
- goto put_old;
-
- ret = -ENOMEM;
- new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vaddr);
- if (!new_page)
- goto put_old;
-
- __SetPageUptodate(new_page);
- copy_highpage(new_page, old_page);
- copy_to_page(new_page, vaddr, &opcode, UPROBE_SWBP_INSN_SIZE);
+ if (unlikely(!folio_test_anon(folio))) {
+ VM_WARN_ON_ONCE(is_register);
+ folio_put(folio);
+ goto out;
+ }
if (!is_register) {
- struct page *orig_page;
- pgoff_t index;
-
- VM_BUG_ON_PAGE(!PageAnon(old_page), old_page);
-
- index = vaddr_to_offset(vma, vaddr & PAGE_MASK) >> PAGE_SHIFT;
- orig_page = find_get_page(vma->vm_file->f_inode->i_mapping,
- index);
-
- if (orig_page) {
- if (PageUptodate(orig_page) &&
- pages_identical(new_page, orig_page)) {
- /* let go new_page */
- put_page(new_page);
- new_page = NULL;
-
- if (PageCompound(orig_page))
- orig_page_huge = true;
- }
- put_page(orig_page);
- }
+ /*
+ * In the common case, we'll be able to zap the page when
+ * unregistering. So trigger MMU notifiers now, as we won't
+ * be able to do it under PTL.
+ */
+ mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm,
+ vaddr, vaddr + PAGE_SIZE);
+ mmu_notifier_invalidate_range_start(&range);
+ }
+
+ ret = -EAGAIN;
+ /* Walk the page tables again, to perform the actual update. */
+ if (folio_walk_start(&fw, vma, vaddr, 0)) {
+ if (fw.page == page)
+ ret = __uprobe_write_opcode(vma, &fw, folio, opcode_vaddr, opcode);
+ folio_walk_end(&fw, vma);
}
- ret = __replace_page(vma, vaddr & PAGE_MASK, old_page, new_page);
- if (new_page)
- put_page(new_page);
-put_old:
- put_page(old_page);
+ if (!is_register)
+ mmu_notifier_invalidate_range_end(&range);
- if (unlikely(ret == -EAGAIN))
+ folio_put(folio);
+ switch (ret) {
+ case -EFAULT:
+ gup_flags |= FOLL_WRITE | FOLL_SPLIT_PMD;
+ fallthrough;
+ case -EAGAIN:
goto retry;
+ default:
+ break;
+ }
+out:
/* Revert back reference counter if instruction update failed. */
- if (ret && is_register && ref_ctr_updated)
+ if (ret < 0 && is_register && ref_ctr_updated)
update_ref_ctr(uprobe, mm, -1);
/* try collapse pmd for compound page */
- if (!ret && orig_page_huge)
+ if (ret > 0)
collapse_pte_mapped_thp(mm, vaddr, false);
- return ret;
+ return ret < 0 ? ret : 0;
}
/**
]
|
| 110/2 |
2026/06/02 16:20 |
llm |
expert |
0m
Model:
Tokens:
input: 59534
output: 26
thoughts: 1852
Thoughts: **Diving into a Memory Accounting Conundrum**
Alright, let's break this down. My initial thought process zeroes in on a potential flaw within `__replace_page`. Specifically, I see how incorrect handling of the zero page could lead to a decrement in `MM_FILEPAGES` when it shouldn't be. This feels like a good lead because I was seeing negative file pages in that bug.
Now, my bug report, that's where things get interesting. I'm looking at `MM_FILEPAGES val:1` and `MM_SWAPENTS val:32`. The `SWAPENTS` value jumps out: 32 swap entries screams "large folio". Could it be that a large, order-5 folio (32 pages) is being swapped out? If it's a file page, it doesn't get swapped, it gets written. But maybe shmem pages are the culprit. Those *can* be swapped. Although, shmem swap entries *aren't* accounted in `MM_SWAPENTS`, are they? I'll need to verify in `shmem_add_to_page_cache` or `shmem_writepage`.
If an anonymous large folio is swapped out, then `MM_ANONPAGES` is decremented by 32, and `MM_SWAPENTS` *is* incremented by 32. If this process exits without the swap entries being freed, that explains the value. However, the `MM_FILEPAGES val:1` is still a puzzle. Are we seeing a leak of a large anon folio *and* a single file page?
Then I had a hunch that perhaps `madvise_free_pte_range` plays a role, particularly given how it interacts with swap entries, although the accounting here seems correct. It *should* be handling negative increments to `nr_swap` correctly after freeing a swap entry.
Next, I thought of `userfaultfd_move` and checked commit `0ea148a79919 mm: userfaultfd: fix race of userfaultfd_move and swap cache`. I saw the mention of folios being accounted to the "wrong VMA". What if a folio *type* is messed up? Imagine an anonymous folio getting moved into a file VMA, or vice-versa! `userfaultfd_move` is about moving pages between VMAs.
But, wait, is that possible? From what I can tell, `userfaultfd_move` only moves within the same type: anonymous memory or shmem, not between the two. However, I see how it might race with swapin. The race condition described in `0ea148a79919` shows a folio "from another VMA".
If a file folio (or shmem) somehow ends up being moved into an anon VMA (or vice versa), the RSS accounting would definitely get messed up. Then the bug report would make perfect sense: `MM_FILEPAGES` messed up to a value of 1 and `MM_SWAPENTS` messed up to a value of 32. This feels like a good match for the bug.
I quickly did a `git log` lookup to verify this, and yes, I found the commit in the tree. The dates seemed off at first, but it *is* a recent commit so I just need to verify that time stamp.
|
| 111/2 |
2026/06/02 16:20 |
tool |
git-show |
0m
Args:
Results: map[Output:commit 0ea148a799198518d8ebab63ddd0bb6114a103bc
Author: Kairui Song <kasong@tencent.com>
Date: Wed Jun 4 23:10:38 2025 +0800
mm: userfaultfd: fix race of userfaultfd_move and swap cache
This commit fixes two kinds of races, they may have different results:
Barry reported a BUG_ON in commit c50f8e6053b0, we may see the same
BUG_ON if the filemap lookup returned NULL and folio is added to swap
cache after that.
If another kind of race is triggered (folio changed after lookup) we
may see RSS counter is corrupted:
[ 406.893936] BUG: Bad rss-counter state mm:ffff0000c5a9ddc0
type:MM_ANONPAGES val:-1
[ 406.894071] BUG: Bad rss-counter state mm:ffff0000c5a9ddc0
type:MM_SHMEMPAGES val:1
Because the folio is being accounted to the wrong VMA.
I'm not sure if there will be any data corruption though, seems no.
The issues above are critical already.
On seeing a swap entry PTE, userfaultfd_move does a lockless swap cache
lookup, and tries to move the found folio to the faulting vma. Currently,
it relies on checking the PTE value to ensure that the moved folio still
belongs to the src swap entry and that no new folio has been added to the
swap cache, which turns out to be unreliable.
While working and reviewing the swap table series with Barry, following
existing races are observed and reproduced [1]:
In the example below, move_pages_pte is moving src_pte to dst_pte, where
src_pte is a swap entry PTE holding swap entry S1, and S1 is not in the
swap cache:
CPU1 CPU2
userfaultfd_move
move_pages_pte()
entry = pte_to_swp_entry(orig_src_pte);
// Here it got entry = S1
... < interrupted> ...
<swapin src_pte, alloc and use folio A>
// folio A is a new allocated folio
// and get installed into src_pte
<frees swap entry S1>
// src_pte now points to folio A, S1
// has swap count == 0, it can be freed
// by folio_swap_swap or swap
// allocator's reclaim.
<try to swap out another folio B>
// folio B is a folio in another VMA.
<put folio B to swap cache using S1 >
// S1 is freed, folio B can use it
// for swap out with no problem.
...
folio = filemap_get_folio(S1)
// Got folio B here !!!
... < interrupted again> ...
<swapin folio B and free S1>
// Now S1 is free to be used again.
<swapout src_pte & folio A using S1>
// Now src_pte is a swap entry PTE
// holding S1 again.
folio_trylock(folio)
move_swap_pte
double_pt_lock
is_pte_pages_stable
// Check passed because src_pte == S1
folio_move_anon_rmap(...)
// Moved invalid folio B here !!!
The race window is very short and requires multiple collisions of multiple
rare events, so it's very unlikely to happen, but with a deliberately
constructed reproducer and increased time window, it can be reproduced
easily.
This can be fixed by checking if the folio returned by filemap is the
valid swap cache folio after acquiring the folio lock.
Another similar race is possible: filemap_get_folio may return NULL, but
folio (A) could be swapped in and then swapped out again using the same
swap entry after the lookup. In such a case, folio (A) may remain in the
swap cache, so it must be moved too:
CPU1 CPU2
userfaultfd_move
move_pages_pte()
entry = pte_to_swp_entry(orig_src_pte);
// Here it got entry = S1, and S1 is not in swap cache
folio = filemap_get_folio(S1)
// Got NULL
... < interrupted again> ...
<swapin folio A and free S1>
<swapout folio A re-using S1>
move_swap_pte
double_pt_lock
is_pte_pages_stable
// Check passed because src_pte == S1
folio_move_anon_rmap(...)
// folio A is ignored !!!
Fix this by checking the swap cache again after acquiring the src_pte
lock. And to avoid the filemap overhead, we check swap_map directly [2].
The SWP_SYNCHRONOUS_IO path does make the problem more complex, but so far
we don't need to worry about that, since folios can only be exposed to the
swap cache in the swap out path, and this is covered in this patch by
checking the swap cache again after acquiring the src_pte lock.
Testing with a simple C program that allocates and moves several GB of
memory did not show any observable performance change.
Link: https://lkml.kernel.org/r/20250604151038.21968-1-ryncsn@gmail.com
Fixes: adef440691ba ("userfaultfd: UFFDIO_MOVE uABI")
Signed-off-by: Kairui Song <kasong@tencent.com>
Closes: https://lore.kernel.org/linux-mm/CAMgjq7B1K=6OOrK2OUZ0-tqCzi+EJt+2_K97TPGoSt=9+JwP7Q@mail.gmail.com/ [1]
Link: https://lore.kernel.org/all/CAGsJ_4yJhJBo16XhiC-nUzSheyX-V3-nFE+tAi=8Y560K8eT=A@mail.gmail.com/ [2]
Reviewed-by: Lokesh Gidra <lokeshgidra@google.com>
Acked-by: Peter Xu <peterx@redhat.com>
Reviewed-by: Suren Baghdasaryan <surenb@google.com>
Reviewed-by: Barry Song <baohua@kernel.org>
Reviewed-by: Chris Li <chrisl@kernel.org>
Cc: Andrea Arcangeli <aarcange@redhat.com>
Cc: David Hildenbrand <david@redhat.com>
Cc: Kairui Song <kasong@tencent.com>
Cc: <stable@vger.kernel.org>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
diff --git a/mm/userfaultfd.c b/mm/userfaultfd.c
index bc473ad21202..8253978ee0fb 100644
--- a/mm/userfaultfd.c
+++ b/mm/userfaultfd.c
@@ -1084,8 +1084,18 @@ static int move_swap_pte(struct mm_struct *mm, struct vm_area_struct *dst_vma,
pte_t orig_dst_pte, pte_t orig_src_pte,
pmd_t *dst_pmd, pmd_t dst_pmdval,
spinlock_t *dst_ptl, spinlock_t *src_ptl,
- struct folio *src_folio)
+ struct folio *src_folio,
+ struct swap_info_struct *si, swp_entry_t entry)
{
+ /*
+ * Check if the folio still belongs to the target swap entry after
+ * acquiring the lock. Folio can be freed in the swap cache while
+ * not locked.
+ */
+ if (src_folio && unlikely(!folio_test_swapcache(src_folio) ||
+ entry.val != src_folio->swap.val))
+ return -EAGAIN;
+
double_pt_lock(dst_ptl, src_ptl);
if (!is_pte_pages_stable(dst_pte, src_pte, orig_dst_pte, orig_src_pte,
@@ -1102,6 +1112,25 @@ static int move_swap_pte(struct mm_struct *mm, struct vm_area_struct *dst_vma,
if (src_folio) {
folio_move_anon_rmap(src_folio, dst_vma);
src_folio->index = linear_page_index(dst_vma, dst_addr);
+ } else {
+ /*
+ * Check if the swap entry is cached after acquiring the src_pte
+ * lock. Otherwise, we might miss a newly loaded swap cache folio.
+ *
+ * Check swap_map directly to minimize overhead, READ_ONCE is sufficient.
+ * We are trying to catch newly added swap cache, the only possible case is
+ * when a folio is swapped in and out again staying in swap cache, using the
+ * same entry before the PTE check above. The PTL is acquired and released
+ * twice, each time after updating the swap_map's flag. So holding
+ * the PTL here ensures we see the updated value. False positive is possible,
+ * e.g. SWP_SYNCHRONOUS_IO swapin may set the flag without touching the
+ * cache, or during the tiny synchronization window between swap cache and
+ * swap_map, but it will be gone very quickly, worst result is retry jitters.
+ */
+ if (READ_ONCE(si->swap_map[swp_offset(entry)]) & SWAP_HAS_CACHE) {
+ double_pt_unlock(dst_ptl, src_ptl);
+ return -EAGAIN;
+ }
}
orig_src_pte = ptep_get_and_clear(mm, src_addr, src_pte);
@@ -1412,7 +1441,7 @@ static int move_pages_pte(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd,
}
err = move_swap_pte(mm, dst_vma, dst_addr, src_addr, dst_pte, src_pte,
orig_dst_pte, orig_src_pte, dst_pmd, dst_pmdval,
- dst_ptl, src_ptl, src_folio);
+ dst_ptl, src_ptl, src_folio, si, entry);
}
out:
]
|
| 112/2 |
2026/06/02 16:20 |
llm |
expert |
0m
Model:
Tokens:
input: 64912
output: 35
thoughts: 361
Thoughts: **Analyzing a Potential RSS Counter Corruption Bug**
Okay, so the commit *is* in the tree, which is a good starting point. The commit message itself points to a race condition that could corrupt the RSS counter, specifically when a folio changes after a lookup is triggered. The message includes the typical "BUG: Bad rss-counter state" errors for both `MM_ANONPAGES` and `MM_SHMEMPAGES`, which is useful.
Now, looking at my own bug report, I see a similar "Bad rss-counter state" error, but for `MM_FILEPAGES` and `MM_SWAPENTS`, and crucially, the commit and bug reports have the same PID for the process that triggered the bug. So, my initial thought is, could this be the same root cause, just manifesting with *different* folio types? It seems like it.
My first guess, the problem relates to `userfaultfd_move` moving a file folio into an anon VMA is wrong. I forgot that `userfaultfd_move` *only* works on anon and shmem VMAs, not file-backed ones.
But, I remember `mfill_atomic` does perform a check for anon or shmem VMs. So, I will check the source code in `mm/userfaultfd.c` to look further.
|
| 113/2 |
2026/06/02 16:20 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/userfaultfd.c Name:mfill_atomic]
Results: map[SourceCode: 704: static __always_inline ssize_t mfill_atomic(struct userfaultfd_ctx *ctx,
705: unsigned long dst_start,
706: unsigned long src_start,
707: unsigned long len,
708: uffd_flags_t flags)
709: {
710: struct mm_struct *dst_mm = ctx->mm;
711: struct vm_area_struct *dst_vma;
712: ssize_t err;
713: pmd_t *dst_pmd;
714: unsigned long src_addr, dst_addr;
715: long copied;
716: struct folio *folio;
717:
718: /*
719: * Sanitize the command parameters:
720: */
721: VM_WARN_ON_ONCE(dst_start & ~PAGE_MASK);
722: VM_WARN_ON_ONCE(len & ~PAGE_MASK);
723:
724: /* Does the address range wrap, or is the span zero-sized? */
725: VM_WARN_ON_ONCE(src_start + len <= src_start);
726: VM_WARN_ON_ONCE(dst_start + len <= dst_start);
727:
728: src_addr = src_start;
729: dst_addr = dst_start;
730: copied = 0;
731: folio = NULL;
732: retry:
733: /*
734: * Make sure the vma is not shared, that the dst range is
735: * both valid and fully within a single existing vma.
736: */
737: dst_vma = uffd_mfill_lock(dst_mm, dst_start, len);
738: if (IS_ERR(dst_vma)) {
739: err = PTR_ERR(dst_vma);
740: goto out;
741: }
742:
743: /*
744: * If memory mappings are changing because of non-cooperative
745: * operation (e.g. mremap) running in parallel, bail out and
746: * request the user to retry later
747: */
748: down_read(&ctx->map_changing_lock);
749: err = -EAGAIN;
750: if (atomic_read(&ctx->mmap_changing))
751: goto out_unlock;
752:
753: err = -EINVAL;
754: /*
755: * shmem_zero_setup is invoked in mmap for MAP_ANONYMOUS|MAP_SHARED but
756: * it will overwrite vm_ops, so vma_is_anonymous must return false.
757: */
758: if (WARN_ON_ONCE(vma_is_anonymous(dst_vma) &&
759: dst_vma->vm_flags & VM_SHARED))
760: goto out_unlock;
761:
762: /*
763: * validate 'mode' now that we know the dst_vma: don't allow
764: * a wrprotect copy if the userfaultfd didn't register as WP.
765: */
766: if ((flags & MFILL_ATOMIC_WP) && !(dst_vma->vm_flags & VM_UFFD_WP))
767: goto out_unlock;
768:
769: /*
770: * If this is a HUGETLB vma, pass off to appropriate routine
771: */
772: if (is_vm_hugetlb_page(dst_vma))
773: return mfill_atomic_hugetlb(ctx, dst_vma, dst_start,
774: src_start, len, flags);
775:
776: if (!vma_is_anonymous(dst_vma) && !vma_is_shmem(dst_vma))
777: goto out_unlock;
778: if (!vma_is_shmem(dst_vma) &&
779: uffd_flags_mode_is(flags, MFILL_ATOMIC_CONTINUE))
780: goto out_unlock;
781:
782: while (src_addr < src_start + len) {
783: pmd_t dst_pmdval;
784:
785: VM_WARN_ON_ONCE(dst_addr >= dst_start + len);
786:
787: dst_pmd = mm_alloc_pmd(dst_mm, dst_addr);
788: if (unlikely(!dst_pmd)) {
789: err = -ENOMEM;
790: break;
791: }
792:
793: dst_pmdval = pmdp_get_lockless(dst_pmd);
794: if (unlikely(pmd_none(dst_pmdval)) &&
795: unlikely(__pte_alloc(dst_mm, dst_pmd))) {
796: err = -ENOMEM;
797: break;
798: }
799: dst_pmdval = pmdp_get_lockless(dst_pmd);
800: /*
801: * If the dst_pmd is THP don't override it and just be strict.
802: * (This includes the case where the PMD used to be THP and
803: * changed back to none after __pte_alloc().)
804: */
805: if (unlikely(!pmd_present(dst_pmdval) ||
806: pmd_trans_huge(dst_pmdval))) {
807: err = -EEXIST;
808: break;
809: }
810: if (unlikely(pmd_bad(dst_pmdval))) {
811: err = -EFAULT;
812: break;
813: }
814: /*
815: * For shmem mappings, khugepaged is allowed to remove page
816: * tables under us; pte_offset_map_lock() will deal with that.
817: */
818:
819: err = mfill_atomic_pte(dst_pmd, dst_vma, dst_addr,
820: src_addr, flags, &folio);
821: cond_resched();
822:
823: if (unlikely(err == -ENOENT)) {
824: void *kaddr;
825:
826: up_read(&ctx->map_changing_lock);
827: uffd_mfill_unlock(dst_vma);
828: VM_WARN_ON_ONCE(!folio);
829:
830: kaddr = kmap_local_folio(folio, 0);
831: err = copy_from_user(kaddr,
832: (const void __user *) src_addr,
833: PAGE_SIZE);
834: kunmap_local(kaddr);
835: if (unlikely(err)) {
836: err = -EFAULT;
837: goto out;
838: }
839: flush_dcache_folio(folio);
840: goto retry;
841: } else
842: VM_WARN_ON_ONCE(folio);
843:
844: if (!err) {
845: dst_addr += PAGE_SIZE;
846: src_addr += PAGE_SIZE;
847: copied += PAGE_SIZE;
848:
849: if (fatal_signal_pending(current))
850: err = -EINTR;
851: }
852: if (err)
853: break;
854: }
855:
856: out_unlock:
857: up_read(&ctx->map_changing_lock);
858: uffd_mfill_unlock(dst_vma);
859: out:
860: if (folio)
861: folio_put(folio);
862: VM_WARN_ON_ONCE(copied < 0);
863: VM_WARN_ON_ONCE(err > 0);
864: VM_WARN_ON_ONCE(!copied && !err);
865: return copied ? copied : err;
866: }
SourceFile:mm/userfaultfd.c]
|
| 114/2 |
2026/06/02 16:20 |
llm |
expert |
0m
Model:
Tokens:
input: 68103
output: 31
thoughts: 417
Thoughts: **My Analysis of Memory Management in the Kernel**
Okay, so I'm looking at `mfill_atomic`. It's pretty straightforward, it seems. The code explicitly checks if the destination VMA is *not* anonymous or shmem. If that's the case, it bails out, `goto out_unlock`. So, it's designed to operate only on anonymous or shared memory VMAs, which makes sense from a high-level perspective. But then, I start thinking about the implications.
Now, what happens if `madvise_free_pte_range` gets called on a shmem VMA? Let's trace it through. The code checks if the page table entry is present and if it finds a swap entry. If it *does* find one, it frees the swap entry, and adjusts the `nr_swap` counter. Then there's `add_mm_counter(mm, MM_SWAPENTS, nr_swap);`.
Here's where things get interesting. I know that shmem swap entries *aren't* accounted for in `MM_SWAPENTS`! When a shmem page gets swapped out, `MM_SHMEMPAGES` gets decremented, but `MM_SWAPENTS` is never incremented. It seems like it's a bug! So it looks like `madvise_free_pte_range` is subtracting swap entries that never got added! That would skew all sorts of accounting.
I need to confirm this. I'll check `shmem_writepage` and `shmem_add_to_page_cache`, see how the page gets swapped. Right, `shmem_writepage` doesn't increment `MM_SWAPENTS`. Let me look at the code in `mm/shmem.c` to see if there is any other place where the counter is incremented when pages are swapped. Something is definitely wrong, and I'll need to dig deeper.
|
| 115/2 |
2026/06/02 16:20 |
tool |
grepper |
0m
Error:
Args: map[Expression:MM_SWAPENTS PathPrefix:mm/shmem.c]
Results:
|
| 116/2 |
2026/06/02 16:20 |
llm |
expert |
0m
Model:
Tokens:
input: 68985
output: 37
thoughts: 89
Thoughts: Okay, let me think this through.
**Analyzing the Swap Behavior of Shared Memory**
Right, so I was looking into how shared memory interacts with swapping and it seems pretty clear, at least at first glance, that `shmem.c` itself doesn't directly manipulate `MM_SWAPENTS`. That makes sense. My understanding is that when a shared memory page gets swapped out, the page table entry (PTE) is replaced by a swap entry. But now I'm remembering that shmem pages are mapped through Virtual Memory Areas (VMAs). Which makes me realize when a shared memory page is swapped out, `try_to_unmap` should be the function called. Now I need to see what's happening at that point. I'll need to check the logic within `try_to_unmap_one` in `mm/rmap.c` now to understand the precise mechanics.
|
| 117/2 |
2026/06/02 16:21 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/rmap.c Name:try_to_unmap_one]
Results: map[SourceCode:1961: /*
1962: * @arg: enum ttu_flags will be passed to this argument
1963: */
1964: static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
1965: unsigned long address, void *arg)
1966: {
1967: struct mm_struct *mm = vma->vm_mm;
1968: DEFINE_FOLIO_VMA_WALK(pvmw, folio, vma, address, 0);
1969: bool anon_exclusive, ret = true;
1970: pte_t pteval;
1971: struct page *subpage;
1972: struct mmu_notifier_range range;
1973: enum ttu_flags flags = (enum ttu_flags)(long)arg;
1974: unsigned long nr_pages = 1, end_addr;
1975: unsigned long pfn;
1976: unsigned long hsz = 0;
1977: int ptes = 0;
1978:
1979: /*
1980: * When racing against e.g. zap_pte_range() on another cpu,
1981: * in between its ptep_get_and_clear_full() and folio_remove_rmap_*(),
1982: * try_to_unmap() may return before page_mapped() has become false,
1983: * if page table locking is skipped: use TTU_SYNC to wait for that.
1984: */
1985: if (flags & TTU_SYNC)
1986: pvmw.flags = PVMW_SYNC;
1987:
1988: /*
1989: * For THP, we have to assume the worse case ie pmd for invalidation.
1990: * For hugetlb, it could be much worse if we need to do pud
1991: * invalidation in the case of pmd sharing.
1992: *
1993: * Note that the folio can not be freed in this function as call of
1994: * try_to_unmap() must hold a reference on the folio.
1995: */
1996: range.end = vma_address_end(&pvmw);
1997: mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma->vm_mm,
1998: address, range.end);
1999: if (folio_test_hugetlb(folio)) {
2000: /*
2001: * If sharing is possible, start and end will be adjusted
2002: * accordingly.
2003: */
2004: adjust_range_if_pmd_sharing_possible(vma, &range.start,
2005: &range.end);
2006:
2007: /* We need the huge page size for set_huge_pte_at() */
2008: hsz = huge_page_size(hstate_vma(vma));
2009: }
2010: mmu_notifier_invalidate_range_start(&range);
2011:
2012: while (page_vma_mapped_walk(&pvmw)) {
2013: /*
2014: * If the folio is in an mlock()d vma, we must not swap it out.
2015: */
2016: if (!(flags & TTU_IGNORE_MLOCK) &&
2017: (vma->vm_flags & VM_LOCKED)) {
2018: ptes++;
2019:
2020: /*
2021: * Set 'ret' to indicate the page cannot be unmapped.
2022: *
2023: * Do not jump to walk_abort immediately as additional
2024: * iteration might be required to detect fully mapped
2025: * folio an mlock it.
2026: */
2027: ret = false;
2028:
2029: /* Only mlock fully mapped pages */
2030: if (pvmw.pte && ptes != pvmw.nr_pages)
2031: continue;
2032:
2033: /*
2034: * All PTEs must be protected by page table lock in
2035: * order to mlock the page.
2036: *
2037: * If page table boundary has been cross, current ptl
2038: * only protect part of ptes.
2039: */
2040: if (pvmw.flags & PVMW_PGTABLE_CROSSED)
2041: goto walk_done;
2042:
2043: /* Restore the mlock which got missed */
2044: mlock_vma_folio(folio, vma);
2045: goto walk_done;
2046: }
2047:
2048: if (!pvmw.pte) {
2049: if (folio_test_anon(folio) && !folio_test_swapbacked(folio)) {
2050: if (unmap_huge_pmd_locked(vma, pvmw.address, pvmw.pmd, folio))
2051: goto walk_done;
2052: /*
2053: * unmap_huge_pmd_locked has either already marked
2054: * the folio as swap-backed or decided to retain it
2055: * due to GUP or speculative references.
2056: */
2057: goto walk_abort;
2058: }
2059:
2060: if (flags & TTU_SPLIT_HUGE_PMD) {
2061: /*
2062: * We temporarily have to drop the PTL and
2063: * restart so we can process the PTE-mapped THP.
2064: */
2065: split_huge_pmd_locked(vma, pvmw.address,
2066: pvmw.pmd, false);
2067: flags &= ~TTU_SPLIT_HUGE_PMD;
2068: page_vma_mapped_walk_restart(&pvmw);
2069: continue;
2070: }
2071: }
2072:
2073: /* Unexpected PMD-mapped THP? */
2074: VM_BUG_ON_FOLIO(!pvmw.pte, folio);
2075:
2076: /*
2077: * Handle PFN swap PTEs, such as device-exclusive ones, that
2078: * actually map pages.
2079: */
2080: pteval = ptep_get(pvmw.pte);
2081: if (likely(pte_present(pteval))) {
2082: pfn = pte_pfn(pteval);
2083: } else {
2084: const softleaf_t entry = softleaf_from_pte(pteval);
2085:
2086: pfn = softleaf_to_pfn(entry);
2087: VM_WARN_ON_FOLIO(folio_test_hugetlb(folio), folio);
2088: }
2089:
2090: subpage = folio_page(folio, pfn - folio_pfn(folio));
2091: address = pvmw.address;
2092: anon_exclusive = folio_test_anon(folio) &&
2093: PageAnonExclusive(subpage);
2094:
2095: if (folio_test_hugetlb(folio)) {
2096: bool anon = folio_test_anon(folio);
2097:
2098: /*
2099: * The try_to_unmap() is only passed a hugetlb page
2100: * in the case where the hugetlb page is poisoned.
2101: */
2102: VM_BUG_ON_PAGE(!PageHWPoison(subpage), subpage);
2103: /*
2104: * huge_pmd_unshare may unmap an entire PMD page.
2105: * There is no way of knowing exactly which PMDs may
2106: * be cached for this mm, so we must flush them all.
2107: * start/end were already adjusted above to cover this
2108: * range.
2109: */
2110: flush_cache_range(vma, range.start, range.end);
2111:
2112: /*
2113: * To call huge_pmd_unshare, i_mmap_rwsem must be
2114: * held in write mode. Caller needs to explicitly
2115: * do this outside rmap routines.
2116: *
2117: * We also must hold hugetlb vma_lock in write mode.
2118: * Lock order dictates acquiring vma_lock BEFORE
2119: * i_mmap_rwsem. We can only try lock here and fail
2120: * if unsuccessful.
2121: */
2122: if (!anon) {
2123: struct mmu_gather tlb;
2124:
2125: VM_BUG_ON(!(flags & TTU_RMAP_LOCKED));
2126: if (!hugetlb_vma_trylock_write(vma))
2127: goto walk_abort;
2128:
2129: tlb_gather_mmu_vma(&tlb, vma);
2130: if (huge_pmd_unshare(&tlb, vma, address, pvmw.pte)) {
2131: hugetlb_vma_unlock_write(vma);
2132: huge_pmd_unshare_flush(&tlb, vma);
2133: tlb_finish_mmu(&tlb);
2134: /*
2135: * The PMD table was unmapped,
2136: * consequently unmapping the folio.
2137: */
2138: goto walk_done;
2139: }
2140: hugetlb_vma_unlock_write(vma);
2141: tlb_finish_mmu(&tlb);
2142: }
2143: pteval = huge_ptep_clear_flush(vma, address, pvmw.pte);
2144: if (pte_dirty(pteval))
2145: folio_mark_dirty(folio);
2146: } else if (likely(pte_present(pteval))) {
2147: nr_pages = folio_unmap_pte_batch(folio, &pvmw, flags, pteval);
2148: end_addr = address + nr_pages * PAGE_SIZE;
2149: flush_cache_range(vma, address, end_addr);
2150:
2151: /* Nuke the page table entry. */
2152: pteval = get_and_clear_ptes(mm, address, pvmw.pte, nr_pages);
2153: /*
2154: * We clear the PTE but do not flush so potentially
2155: * a remote CPU could still be writing to the folio.
2156: * If the entry was previously clean then the
2157: * architecture must guarantee that a clear->dirty
2158: * transition on a cached TLB entry is written through
2159: * and traps if the PTE is unmapped.
2160: */
2161: if (should_defer_flush(mm, flags))
2162: set_tlb_ubc_flush_pending(mm, pteval, address, end_addr);
2163: else
2164: flush_tlb_range(vma, address, end_addr);
2165: if (pte_dirty(pteval))
2166: folio_mark_dirty(folio);
2167: } else {
2168: pte_clear(mm, address, pvmw.pte);
2169: }
2170:
2171: /*
2172: * Now the pte is cleared. If this pte was uffd-wp armed,
2173: * we may want to replace a none pte with a marker pte if
2174: * it's file-backed, so we don't lose the tracking info.
2175: */
2176: pte_install_uffd_wp_if_needed(vma, address, pvmw.pte, pteval);
2177:
2178: /* Update high watermark before we lower rss */
2179: update_hiwater_rss(mm);
2180:
2181: if (PageHWPoison(subpage) && (flags & TTU_HWPOISON)) {
2182: pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));
2183: if (folio_test_hugetlb(folio)) {
2184: hugetlb_count_sub(folio_nr_pages(folio), mm);
2185: set_huge_pte_at(mm, address, pvmw.pte, pteval,
2186: hsz);
2187: } else {
2188: dec_mm_counter(mm, mm_counter(folio));
2189: set_pte_at(mm, address, pvmw.pte, pteval);
2190: }
2191: } else if (likely(pte_present(pteval)) && pte_unused(pteval) &&
2192: !userfaultfd_armed(vma)) {
2193: /*
2194: * The guest indicated that the page content is of no
2195: * interest anymore. Simply discard the pte, vmscan
2196: * will take care of the rest.
2197: * A future reference will then fault in a new zero
2198: * page. When userfaultfd is active, we must not drop
2199: * this page though, as its main user (postcopy
2200: * migration) will not expect userfaults on already
2201: * copied pages.
2202: */
2203: dec_mm_counter(mm, mm_counter(folio));
2204: } else if (folio_test_anon(folio)) {
2205: swp_entry_t entry = page_swap_entry(subpage);
2206: pte_t swp_pte;
2207: /*
2208: * Store the swap location in the pte.
2209: * See handle_pte_fault() ...
2210: */
2211: if (unlikely(folio_test_swapbacked(folio) !=
2212: folio_test_swapcache(folio))) {
2213: WARN_ON_ONCE(1);
2214: goto walk_abort;
2215: }
2216:
2217: /* MADV_FREE page check */
2218: if (!folio_test_swapbacked(folio)) {
2219: int ref_count, map_count;
2220:
2221: /*
2222: * Synchronize with gup_pte_range():
2223: * - clear PTE; barrier; read refcount
2224: * - inc refcount; barrier; read PTE
2225: */
2226: smp_mb();
2227:
2228: ref_count = folio_ref_count(folio);
2229: map_count = folio_mapcount(folio);
2230:
2231: /*
2232: * Order reads for page refcount and dirty flag
2233: * (see comments in __remove_mapping()).
2234: */
2235: smp_rmb();
2236:
2237: if (folio_test_dirty(folio) && !(vma->vm_flags & VM_DROPPABLE)) {
2238: /*
2239: * redirtied either using the page table or a previously
2240: * obtained GUP reference.
2241: */
2242: set_ptes(mm, address, pvmw.pte, pteval, nr_pages);
2243: folio_set_swapbacked(folio);
2244: goto walk_abort;
2245: } else if (ref_count != 1 + map_count) {
2246: /*
2247: * Additional reference. Could be a GUP reference or any
2248: * speculative reference. GUP users must mark the folio
2249: * dirty if there was a modification. This folio cannot be
2250: * reclaimed right now either way, so act just like nothing
2251: * happened.
2252: * We'll come back here later and detect if the folio was
2253: * dirtied when the additional reference is gone.
2254: */
2255: set_ptes(mm, address, pvmw.pte, pteval, nr_pages);
2256: goto walk_abort;
2257: }
2258: add_mm_counter(mm, MM_ANONPAGES, -nr_pages);
2259: goto discard;
2260: }
2261:
2262: if (folio_dup_swap(folio, subpage) < 0) {
2263: set_pte_at(mm, address, pvmw.pte, pteval);
2264: goto walk_abort;
2265: }
2266:
2267: /*
2268: * arch_unmap_one() is expected to be a NOP on
2269: * architectures where we could have PFN swap PTEs,
2270: * so we'll not check/care.
2271: */
2272: if (arch_unmap_one(mm, vma, address, pteval) < 0) {
2273: folio_put_swap(folio, subpage);
2274: set_pte_at(mm, address, pvmw.pte, pteval);
2275: goto walk_abort;
2276: }
2277:
2278: /* See folio_try_share_anon_rmap(): clear PTE first. */
2279: if (anon_exclusive &&
2280: folio_try_share_anon_rmap_pte(folio, subpage)) {
2281: folio_put_swap(folio, subpage);
2282: set_pte_at(mm, address, pvmw.pte, pteval);
2283: goto walk_abort;
2284: }
2285: if (list_empty(&mm->mmlist)) {
2286: spin_lock(&mmlist_lock);
2287: if (list_empty(&mm->mmlist))
2288: list_add(&mm->mmlist, &init_mm.mmlist);
2289: spin_unlock(&mmlist_lock);
2290: }
2291: dec_mm_counter(mm, MM_ANONPAGES);
2292: inc_mm_counter(mm, MM_SWAPENTS);
2293: swp_pte = swp_entry_to_pte(entry);
2294: if (anon_exclusive)
2295: swp_pte = pte_swp_mkexclusive(swp_pte);
2296: if (likely(pte_present(pteval))) {
2297: if (pte_soft_dirty(pteval))
2298: swp_pte = pte_swp_mksoft_dirty(swp_pte);
2299: if (pte_uffd_wp(pteval))
2300: swp_pte = pte_swp_mkuffd_wp(swp_pte);
2301: } else {
2302: if (pte_swp_soft_dirty(pteval))
2303: swp_pte = pte_swp_mksoft_dirty(swp_pte);
2304: if (pte_swp_uffd_wp(pteval))
2305: swp_pte = pte_swp_mkuffd_wp(swp_pte);
2306: }
2307: set_pte_at(mm, address, pvmw.pte, swp_pte);
2308: } else {
2309: /*
2310: * This is a locked file-backed folio,
2311: * so it cannot be removed from the page
2312: * cache and replaced by a new folio before
2313: * mmu_notifier_invalidate_range_end, so no
2314: * concurrent thread might update its page table
2315: * to point at a new folio while a device is
2316: * still using this folio.
2317: *
2318: * See Documentation/mm/mmu_notifier.rst
2319: */
2320: add_mm_counter(mm, mm_counter_file(folio), -nr_pages);
2321: }
2322: discard:
2323: if (unlikely(folio_test_hugetlb(folio))) {
2324: hugetlb_remove_rmap(folio);
2325: } else {
2326: folio_remove_rmap_ptes(folio, subpage, nr_pages, vma);
2327: }
2328: if (vma->vm_flags & VM_LOCKED)
2329: mlock_drain_local();
2330: folio_put_refs(folio, nr_pages);
2331:
2332: /*
2333: * If we are sure that we batched the entire folio and cleared
2334: * all PTEs, we can just optimize and stop right here.
2335: */
2336: if (nr_pages == folio_nr_pages(folio))
2337: goto walk_done;
2338: continue;
2339: walk_abort:
2340: ret = false;
2341: walk_done:
2342: page_vma_mapped_walk_done(&pvmw);
2343: break;
2344: }
2345:
2346: mmu_notifier_invalidate_range_end(&range);
2347:
2348: return ret;
2349: }
SourceFile:mm/rmap.c]
|
| 118/2 |
2026/06/02 16:21 |
llm |
expert |
0m
Model:
Tokens:
input: 75572
output: 37
thoughts: 5497
Thoughts: **Delving into the Depths of Memory Accounting and Swap Operations**
Right, let's break this down. My initial thought process revolves around the `MM_SWAPENTS` counter. It's only incremented for anonymous folios during the unmapping process, specifically in `try_to_unmap_one`. This seems straightforward, but what if a userfaultfd moves a file folio into an anonymous VMA and then that's swapped out? The move itself doesn't make the file folio anonymous. `folio_test_anon(folio)` remains false, even though the page is *in* an anon VMA. However, `folio_move_anon_rmap` expects an anon folio, and calling it on a file folio seems like a recipe for rmap corruption, as the commit `0ea148a79919` suggests. This potential rmap corruption due to incorrect VMA accounting is a real concern.
Now, if we consider `move_pages_pte`, which *should* be the same `mm`, there isn't a modification of the RSS counters, so it shouldn't be corrupted, which leads me to my next assumption.
Then, the focus shifts to `userfaultfd_move`. This function *doesn't* touch RSS counters directly, but it *can* move a file page into an anonymous VMA, which could be where `zap_pte_range` comes into play. If it moved it into an anon VMA, and it's a shmem folio, then during the unmapping (`zap_pte_range`), it decrements `MM_SHMEMPAGES`, but the folio was *already* counted as `MM_SHMEMPAGES`, so again, no change to accounting. So why is the commit saying it's the wrong VMA?
I am now focusing on the move of swap entries. The function `move_swap_pte` does not touch RSS counters, so this isn't my culprit, but what about moving shmem swap entries? If the swap entry's target is shmem, the move won't corrupt, because `userfaultfd_move` *only* works between anon and shmem VMAs, meaning moving a swap entry from shmem to anon VMA could be where the bug lies. It explicitly checks this: `vma_is_anonymous(dst_vma) != vma_is_anonymous(src_vma)` - it returns `-EINVAL`. This would prevent the RSS counters from corrupting, right? So the puzzle pieces aren't adding up.
The critical piece seems to be the potential race condition described in that commit. If a folio is reallocated *as* a shmem folio, and `userfaultfd_move` is raced with a swapout, then it is moved to a VMA where `folio_move_anon_rmap` could be a problem, since shmem folios aren't in the swap cache but in the shmem inode page cache. However, Shmem *can* be swapped out, meaning the swap address space can return a shmem folio, which, during an unmap, could lead to corruption in the RSS. This is where a shmem folio *in* the swap cache could introduce the race.
The bug report says `MM_FILEPAGES val:1` and `MM_SWAPENTS val:32`. I am confused because, I am led to believe that `MM_FILEPAGES` is for file folios which are not shmem, and therefore, should *not* be in the swap cache. File folios *cannot* exist in the swap cache, because `folio_test_swapbacked` is false. This means `MM_FILEPAGES` cannot be corrupted by this race, so what could lead to that state?
I have to rethink how `MM_FILEPAGES val:1` and `MM_SWAPENTS val:32` could happen. What might cause these values to change? Are `MM_FILEPAGES` and `MM_SWAPENTS` updated together *anywhere*? If a swap entry points to a file folio, and `do_swap_page` gets called on a file VMA, which increments `MM_ANONPAGES` and decrements `MM_SWAPENTS`, then, on unmap, `zap_pte_range` would decrement `MM_ANONPAGES` and this would be balanced, so the bug isn't in that case. I still don't have an explanation for these two counters being unbalanced. `MM_SWAPENTS` is incremented upon swapin/swapout, but how do swap entries get *inserted* into file VMAs? It could be from an error, in which case the counter would not increment, which would result in `MM
|
| 119/2 |
2026/06/02 16:21 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/rmap.c Name:try_to_unmap_one]
Results: map[SourceCode:1961: /*
1962: * @arg: enum ttu_flags will be passed to this argument
1963: */
1964: static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
1965: unsigned long address, void *arg)
1966: {
1967: struct mm_struct *mm = vma->vm_mm;
1968: DEFINE_FOLIO_VMA_WALK(pvmw, folio, vma, address, 0);
1969: bool anon_exclusive, ret = true;
1970: pte_t pteval;
1971: struct page *subpage;
1972: struct mmu_notifier_range range;
1973: enum ttu_flags flags = (enum ttu_flags)(long)arg;
1974: unsigned long nr_pages = 1, end_addr;
1975: unsigned long pfn;
1976: unsigned long hsz = 0;
1977: int ptes = 0;
1978:
1979: /*
1980: * When racing against e.g. zap_pte_range() on another cpu,
1981: * in between its ptep_get_and_clear_full() and folio_remove_rmap_*(),
1982: * try_to_unmap() may return before page_mapped() has become false,
1983: * if page table locking is skipped: use TTU_SYNC to wait for that.
1984: */
1985: if (flags & TTU_SYNC)
1986: pvmw.flags = PVMW_SYNC;
1987:
1988: /*
1989: * For THP, we have to assume the worse case ie pmd for invalidation.
1990: * For hugetlb, it could be much worse if we need to do pud
1991: * invalidation in the case of pmd sharing.
1992: *
1993: * Note that the folio can not be freed in this function as call of
1994: * try_to_unmap() must hold a reference on the folio.
1995: */
1996: range.end = vma_address_end(&pvmw);
1997: mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma->vm_mm,
1998: address, range.end);
1999: if (folio_test_hugetlb(folio)) {
2000: /*
2001: * If sharing is possible, start and end will be adjusted
2002: * accordingly.
2003: */
2004: adjust_range_if_pmd_sharing_possible(vma, &range.start,
2005: &range.end);
2006:
2007: /* We need the huge page size for set_huge_pte_at() */
2008: hsz = huge_page_size(hstate_vma(vma));
2009: }
2010: mmu_notifier_invalidate_range_start(&range);
2011:
2012: while (page_vma_mapped_walk(&pvmw)) {
2013: /*
2014: * If the folio is in an mlock()d vma, we must not swap it out.
2015: */
2016: if (!(flags & TTU_IGNORE_MLOCK) &&
2017: (vma->vm_flags & VM_LOCKED)) {
2018: ptes++;
2019:
2020: /*
2021: * Set 'ret' to indicate the page cannot be unmapped.
2022: *
2023: * Do not jump to walk_abort immediately as additional
2024: * iteration might be required to detect fully mapped
2025: * folio an mlock it.
2026: */
2027: ret = false;
2028:
2029: /* Only mlock fully mapped pages */
2030: if (pvmw.pte && ptes != pvmw.nr_pages)
2031: continue;
2032:
2033: /*
2034: * All PTEs must be protected by page table lock in
2035: * order to mlock the page.
2036: *
2037: * If page table boundary has been cross, current ptl
2038: * only protect part of ptes.
2039: */
2040: if (pvmw.flags & PVMW_PGTABLE_CROSSED)
2041: goto walk_done;
2042:
2043: /* Restore the mlock which got missed */
2044: mlock_vma_folio(folio, vma);
2045: goto walk_done;
2046: }
2047:
2048: if (!pvmw.pte) {
2049: if (folio_test_anon(folio) && !folio_test_swapbacked(folio)) {
2050: if (unmap_huge_pmd_locked(vma, pvmw.address, pvmw.pmd, folio))
2051: goto walk_done;
2052: /*
2053: * unmap_huge_pmd_locked has either already marked
2054: * the folio as swap-backed or decided to retain it
2055: * due to GUP or speculative references.
2056: */
2057: goto walk_abort;
2058: }
2059:
2060: if (flags & TTU_SPLIT_HUGE_PMD) {
2061: /*
2062: * We temporarily have to drop the PTL and
2063: * restart so we can process the PTE-mapped THP.
2064: */
2065: split_huge_pmd_locked(vma, pvmw.address,
2066: pvmw.pmd, false);
2067: flags &= ~TTU_SPLIT_HUGE_PMD;
2068: page_vma_mapped_walk_restart(&pvmw);
2069: continue;
2070: }
2071: }
2072:
2073: /* Unexpected PMD-mapped THP? */
2074: VM_BUG_ON_FOLIO(!pvmw.pte, folio);
2075:
2076: /*
2077: * Handle PFN swap PTEs, such as device-exclusive ones, that
2078: * actually map pages.
2079: */
2080: pteval = ptep_get(pvmw.pte);
2081: if (likely(pte_present(pteval))) {
2082: pfn = pte_pfn(pteval);
2083: } else {
2084: const softleaf_t entry = softleaf_from_pte(pteval);
2085:
2086: pfn = softleaf_to_pfn(entry);
2087: VM_WARN_ON_FOLIO(folio_test_hugetlb(folio), folio);
2088: }
2089:
2090: subpage = folio_page(folio, pfn - folio_pfn(folio));
2091: address = pvmw.address;
2092: anon_exclusive = folio_test_anon(folio) &&
2093: PageAnonExclusive(subpage);
2094:
2095: if (folio_test_hugetlb(folio)) {
2096: bool anon = folio_test_anon(folio);
2097:
2098: /*
2099: * The try_to_unmap() is only passed a hugetlb page
2100: * in the case where the hugetlb page is poisoned.
2101: */
2102: VM_BUG_ON_PAGE(!PageHWPoison(subpage), subpage);
2103: /*
2104: * huge_pmd_unshare may unmap an entire PMD page.
2105: * There is no way of knowing exactly which PMDs may
2106: * be cached for this mm, so we must flush them all.
2107: * start/end were already adjusted above to cover this
2108: * range.
2109: */
2110: flush_cache_range(vma, range.start, range.end);
2111:
2112: /*
2113: * To call huge_pmd_unshare, i_mmap_rwsem must be
2114: * held in write mode. Caller needs to explicitly
2115: * do this outside rmap routines.
2116: *
2117: * We also must hold hugetlb vma_lock in write mode.
2118: * Lock order dictates acquiring vma_lock BEFORE
2119: * i_mmap_rwsem. We can only try lock here and fail
2120: * if unsuccessful.
2121: */
2122: if (!anon) {
2123: struct mmu_gather tlb;
2124:
2125: VM_BUG_ON(!(flags & TTU_RMAP_LOCKED));
2126: if (!hugetlb_vma_trylock_write(vma))
2127: goto walk_abort;
2128:
2129: tlb_gather_mmu_vma(&tlb, vma);
2130: if (huge_pmd_unshare(&tlb, vma, address, pvmw.pte)) {
2131: hugetlb_vma_unlock_write(vma);
2132: huge_pmd_unshare_flush(&tlb, vma);
2133: tlb_finish_mmu(&tlb);
2134: /*
2135: * The PMD table was unmapped,
2136: * consequently unmapping the folio.
2137: */
2138: goto walk_done;
2139: }
2140: hugetlb_vma_unlock_write(vma);
2141: tlb_finish_mmu(&tlb);
2142: }
2143: pteval = huge_ptep_clear_flush(vma, address, pvmw.pte);
2144: if (pte_dirty(pteval))
2145: folio_mark_dirty(folio);
2146: } else if (likely(pte_present(pteval))) {
2147: nr_pages = folio_unmap_pte_batch(folio, &pvmw, flags, pteval);
2148: end_addr = address + nr_pages * PAGE_SIZE;
2149: flush_cache_range(vma, address, end_addr);
2150:
2151: /* Nuke the page table entry. */
2152: pteval = get_and_clear_ptes(mm, address, pvmw.pte, nr_pages);
2153: /*
2154: * We clear the PTE but do not flush so potentially
2155: * a remote CPU could still be writing to the folio.
2156: * If the entry was previously clean then the
2157: * architecture must guarantee that a clear->dirty
2158: * transition on a cached TLB entry is written through
2159: * and traps if the PTE is unmapped.
2160: */
2161: if (should_defer_flush(mm, flags))
2162: set_tlb_ubc_flush_pending(mm, pteval, address, end_addr);
2163: else
2164: flush_tlb_range(vma, address, end_addr);
2165: if (pte_dirty(pteval))
2166: folio_mark_dirty(folio);
2167: } else {
2168: pte_clear(mm, address, pvmw.pte);
2169: }
2170:
2171: /*
2172: * Now the pte is cleared. If this pte was uffd-wp armed,
2173: * we may want to replace a none pte with a marker pte if
2174: * it's file-backed, so we don't lose the tracking info.
2175: */
2176: pte_install_uffd_wp_if_needed(vma, address, pvmw.pte, pteval);
2177:
2178: /* Update high watermark before we lower rss */
2179: update_hiwater_rss(mm);
2180:
2181: if (PageHWPoison(subpage) && (flags & TTU_HWPOISON)) {
2182: pteval = swp_entry_to_pte(make_hwpoison_entry(subpage));
2183: if (folio_test_hugetlb(folio)) {
2184: hugetlb_count_sub(folio_nr_pages(folio), mm);
2185: set_huge_pte_at(mm, address, pvmw.pte, pteval,
2186: hsz);
2187: } else {
2188: dec_mm_counter(mm, mm_counter(folio));
2189: set_pte_at(mm, address, pvmw.pte, pteval);
2190: }
2191: } else if (likely(pte_present(pteval)) && pte_unused(pteval) &&
2192: !userfaultfd_armed(vma)) {
2193: /*
2194: * The guest indicated that the page content is of no
2195: * interest anymore. Simply discard the pte, vmscan
2196: * will take care of the rest.
2197: * A future reference will then fault in a new zero
2198: * page. When userfaultfd is active, we must not drop
2199: * this page though, as its main user (postcopy
2200: * migration) will not expect userfaults on already
2201: * copied pages.
2202: */
2203: dec_mm_counter(mm, mm_counter(folio));
2204: } else if (folio_test_anon(folio)) {
2205: swp_entry_t entry = page_swap_entry(subpage);
2206: pte_t swp_pte;
2207: /*
2208: * Store the swap location in the pte.
2209: * See handle_pte_fault() ...
2210: */
2211: if (unlikely(folio_test_swapbacked(folio) !=
2212: folio_test_swapcache(folio))) {
2213: WARN_ON_ONCE(1);
2214: goto walk_abort;
2215: }
2216:
2217: /* MADV_FREE page check */
2218: if (!folio_test_swapbacked(folio)) {
2219: int ref_count, map_count;
2220:
2221: /*
2222: * Synchronize with gup_pte_range():
2223: * - clear PTE; barrier; read refcount
2224: * - inc refcount; barrier; read PTE
2225: */
2226: smp_mb();
2227:
2228: ref_count = folio_ref_count(folio);
2229: map_count = folio_mapcount(folio);
2230:
2231: /*
2232: * Order reads for page refcount and dirty flag
2233: * (see comments in __remove_mapping()).
2234: */
2235: smp_rmb();
2236:
2237: if (folio_test_dirty(folio) && !(vma->vm_flags & VM_DROPPABLE)) {
2238: /*
2239: * redirtied either using the page table or a previously
2240: * obtained GUP reference.
2241: */
2242: set_ptes(mm, address, pvmw.pte, pteval, nr_pages);
2243: folio_set_swapbacked(folio);
2244: goto walk_abort;
2245: } else if (ref_count != 1 + map_count) {
2246: /*
2247: * Additional reference. Could be a GUP reference or any
2248: * speculative reference. GUP users must mark the folio
2249: * dirty if there was a modification. This folio cannot be
2250: * reclaimed right now either way, so act just like nothing
2251: * happened.
2252: * We'll come back here later and detect if the folio was
2253: * dirtied when the additional reference is gone.
2254: */
2255: set_ptes(mm, address, pvmw.pte, pteval, nr_pages);
2256: goto walk_abort;
2257: }
2258: add_mm_counter(mm, MM_ANONPAGES, -nr_pages);
2259: goto discard;
2260: }
2261:
2262: if (folio_dup_swap(folio, subpage) < 0) {
2263: set_pte_at(mm, address, pvmw.pte, pteval);
2264: goto walk_abort;
2265: }
2266:
2267: /*
2268: * arch_unmap_one() is expected to be a NOP on
2269: * architectures where we could have PFN swap PTEs,
2270: * so we'll not check/care.
2271: */
2272: if (arch_unmap_one(mm, vma, address, pteval) < 0) {
2273: folio_put_swap(folio, subpage);
2274: set_pte_at(mm, address, pvmw.pte, pteval);
2275: goto walk_abort;
2276: }
2277:
2278: /* See folio_try_share_anon_rmap(): clear PTE first. */
2279: if (anon_exclusive &&
2280: folio_try_share_anon_rmap_pte(folio, subpage)) {
2281: folio_put_swap(folio, subpage);
2282: set_pte_at(mm, address, pvmw.pte, pteval);
2283: goto walk_abort;
2284: }
2285: if (list_empty(&mm->mmlist)) {
2286: spin_lock(&mmlist_lock);
2287: if (list_empty(&mm->mmlist))
2288: list_add(&mm->mmlist, &init_mm.mmlist);
2289: spin_unlock(&mmlist_lock);
2290: }
2291: dec_mm_counter(mm, MM_ANONPAGES);
2292: inc_mm_counter(mm, MM_SWAPENTS);
2293: swp_pte = swp_entry_to_pte(entry);
2294: if (anon_exclusive)
2295: swp_pte = pte_swp_mkexclusive(swp_pte);
2296: if (likely(pte_present(pteval))) {
2297: if (pte_soft_dirty(pteval))
2298: swp_pte = pte_swp_mksoft_dirty(swp_pte);
2299: if (pte_uffd_wp(pteval))
2300: swp_pte = pte_swp_mkuffd_wp(swp_pte);
2301: } else {
2302: if (pte_swp_soft_dirty(pteval))
2303: swp_pte = pte_swp_mksoft_dirty(swp_pte);
2304: if (pte_swp_uffd_wp(pteval))
2305: swp_pte = pte_swp_mkuffd_wp(swp_pte);
2306: }
2307: set_pte_at(mm, address, pvmw.pte, swp_pte);
2308: } else {
2309: /*
2310: * This is a locked file-backed folio,
2311: * so it cannot be removed from the page
2312: * cache and replaced by a new folio before
2313: * mmu_notifier_invalidate_range_end, so no
2314: * concurrent thread might update its page table
2315: * to point at a new folio while a device is
2316: * still using this folio.
2317: *
2318: * See Documentation/mm/mmu_notifier.rst
2319: */
2320: add_mm_counter(mm, mm_counter_file(folio), -nr_pages);
2321: }
2322: discard:
2323: if (unlikely(folio_test_hugetlb(folio))) {
2324: hugetlb_remove_rmap(folio);
2325: } else {
2326: folio_remove_rmap_ptes(folio, subpage, nr_pages, vma);
2327: }
2328: if (vma->vm_flags & VM_LOCKED)
2329: mlock_drain_local();
2330: folio_put_refs(folio, nr_pages);
2331:
2332: /*
2333: * If we are sure that we batched the entire folio and cleared
2334: * all PTEs, we can just optimize and stop right here.
2335: */
2336: if (nr_pages == folio_nr_pages(folio))
2337: goto walk_done;
2338: continue;
2339: walk_abort:
2340: ret = false;
2341: walk_done:
2342: page_vma_mapped_walk_done(&pvmw);
2343: break;
2344: }
2345:
2346: mmu_notifier_invalidate_range_end(&range);
2347:
2348: return ret;
2349: }
SourceFile:mm/rmap.c]
|
| 120/2 |
2026/06/02 16:21 |
llm |
expert |
0m
Model:
Tokens:
input: 88401
output: 36
thoughts: 126
Thoughts: **Analyzing Memory Management Counter Updates**
Okay, hold on a second! I'm looking at this code snippet, and something immediately jumps out at me. We're dealing with anonymous pages here, specifically, those related to a folio that has passed the `folio_test_anon` check. The code then calls `dec_mm_counter` and `inc_mm_counter`. But, wait a minute, I know these functions typically only modify the counters by a value of 1. My concern is, what if `folio` represents a huge page (mTHP)? If it's a huge page, then `nr_pages` could very well be greater than 1! We are not accounting for `nr_pages` and are undercounting or overcounting. I need to dig into the implementation of `dec_mm_counter` and `inc_mm_counter` and confirm this.
|
| 121/2 |
2026/06/02 16:21 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:include/linux/mm.h Name:dec_mm_counter]
Results: map[SourceCode:3089: static inline void dec_mm_counter(struct mm_struct *mm, int member)
3090: {
3091: percpu_counter_dec(&mm->rss_stat[member]);
3092:
3093: mm_trace_rss_stat(mm, member);
3094: }
SourceFile:include/linux/mm.h]
|
| 122/2 |
2026/06/02 16:21 |
llm |
expert |
0m
Model:
Tokens:
input: 88872
output: 37
thoughts: 1046
Thoughts: **Unraveling a Potential Memory Accounting Issue with mTHP Swapout**
Okay, so I'm diving into this code, and it seems like `dec_mm_counter` and `inc_mm_counter` are only ever incrementing or decrementing by one, as expected. But then I see that `nr_pages` is calculated using `folio_unmap_pte_batch`. This *could* result in a value greater than one, which is where things get interesting. Specifically, if `nr_pages` is, say, 32, then `try_to_unmap_one` might be unmapping 32 pages at once. The problem is that, even with this batching, `MM_ANONPAGES` is only decremented by one and `MM_SWAPENTS` is only incremented by one.
I need to confirm if `try_to_unmap_one` is even called during mTHP swapout and from what I can see, `shrink_folio_list` calls `try_to_unmap` for mTHP.
My immediate thought is that `MM_SWAPENTS` would be under-accounted. And then I remember the bug report, which lists `MM_SWAPENTS val:32`. I now realize, if it is under-accounted, then when the VMA is eventually unmapped, `zap_pte_range` will decrement `MM_SWAPENTS` by 32! That could lead to a negative value. However, the reported value is positive.
Hold on, maybe it's only happening with anonymous pages. Let me look at the code. If `try_to_unmap_one` is called for a file folio, it correctly uses `add_mm_counter(..., -nr_pages)`. That's good. And the same is true for `MADV_FREE` calls. It correctly uses `add_mm_counter(..., -nr_pages)`.
But with swapout:
```c
dec_mm_counter(mm, MM_ANONPAGES);
inc_mm_counter(mm, MM_SWAPENTS);
```
Wait! So the batching behavior of `folio_unmap_pte_batch` is only accounting for a single page, even if it unmaps multiple. Time to double-check if `folio_unmap_pte_batch` can return a number greater than one for swapout operations. Yes, I can see that it can.
Ok, so it unmaps multiple PTEs (the `nr_pages` part), then it calls `set_pte_at`, but `set_pte_at` only sets one PTE, `pvmw.pte`. This means the others are left as NONE. Is this true? I check the code, and yes, I see the call to `set_pte_at` and it is only setting `pvmw.pte`.
If `nr_pages` is greater than one, `pvmw.pte` is just the first PTE. I see that the other PTEs are cleared by `get_and_clear_ptes(mm, address, pvmw.pte, nr_pages)`. If they are cleared, and left as NONE, that's it! `nr_pages - 1` pages are effectively lost, right?
But, wait... `try_to_unmap_one` also calls `folio_dup_swap`. And that only duplicates *one* swap entry! So this batching isn't really batching the swap entry at all.
This means that `try_to_unmap_one` doesn't support batching for swapout, at least not correctly! I need to investigate this `folio_unmap_pte_batch` function further.
|
| 123/2 |
2026/06/02 16:22 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/rmap.c Name:folio_unmap_pte_batch]
Results: map[SourceCode:1931: static inline unsigned int folio_unmap_pte_batch(struct folio *folio,
1932: struct page_vma_mapped_walk *pvmw,
1933: enum ttu_flags flags, pte_t pte)
1934: {
1935: unsigned long end_addr, addr = pvmw->address;
1936: struct vm_area_struct *vma = pvmw->vma;
1937: unsigned int max_nr;
1938:
1939: if (flags & TTU_HWPOISON)
1940: return 1;
1941: if (!folio_test_large(folio))
1942: return 1;
1943:
1944: /* We may only batch within a single VMA and a single page table. */
1945: end_addr = pmd_addr_end(addr, vma->vm_end);
1946: max_nr = (end_addr - addr) >> PAGE_SHIFT;
1947:
1948: /* We only support lazyfree or file folios batching for now ... */
1949: if (folio_test_anon(folio) && folio_test_swapbacked(folio))
1950: return 1;
1951:
1952: if (pte_unused(pte))
1953: return 1;
1954:
1955: if (userfaultfd_wp(vma))
1956: return 1;
1957:
1958: return folio_pte_batch(folio, pvmw->pte, pte, max_nr);
1959: }
SourceFile:mm/rmap.c]
|
| 124/2 |
2026/06/02 16:22 |
llm |
expert |
0m
Model:
Tokens:
input: 91265
output: 39
thoughts: 783
Thoughts: **Analyzing Memory Accounting in Linux Page Management**
Okay, let's break this down. My initial observation about `folio_unmap_pte_batch` is key. The code clearly indicates that it *skips* batching for anonymous, swap-backed folios. It returns 1 in this scenario. This directly implies that swap-out operations aren't batched, and therefore, the counters for memory management are handled correctly. `nr_pages` is correctly set to 1, ensuring accurate decrement and increment of memory counters.
However, the logic extends beyond swap-backed anon folios. If a folio *is* anonymous but *not* swap-backed, it's flagged as a lazyfree folio (due to `MADV_FREE`). Now, `folio_unmap_pte_batch` *can* return a value greater than 1 in this lazyfree context. The function `try_to_unmap_one` then correctly handles this using `add_mm_counter(mm, MM_ANONPAGES, -nr_pages)`, ensuring the `MM_ANONPAGES` counter is decremented appropriately.
For file folios, the code is equally sound: `add_mm_counter(mm, mm_counter_file(folio), -nr_pages)` ensures file-backed page accounting is also correct. So, the `try_to_unmap_one` function itself seems to be accounting for memory changes accurately, across both anon and file mappings.
Now, let's revisit the relationship between `MM_FILEPAGES`, `MM_SWAPENTS` and `madvise_free_pte_range`. I suspect `madvise_free_pte_range` may play a part. Inside this, I find that if a swap entry is found, it will decrement `nr_swap` by `nr`. Subsequently `add_mm_counter(mm, MM_SWAPENTS, nr_swap)` will decrement `MM_SWAPENTS`.
But, a critical question arises: What if `madvise_free_pte_range` encounters a swap entry associated with a shared memory (shmem) file? Because shmem supports `MADV_FREE`, I need to check. Let's look into the `madvise_vma_behavior` function. Okay, here we go, under the `MADV_FREE` case: it's immediately apparent that `MADV_FREE` *is only* supported for anonymous VMAs and NOT shmem VMAs.
Therefore, `madvise_free_pte_range` *won't* be called on a shmem VMA with `MADV_FREE`, as that is unsupported. Hence the memory accounting logic should not have any issues in this scenario.
|
| 125/2 |
2026/06/02 16:22 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/madvise.c Name:madvise_vma_behavior]
Results: map[SourceCode:1340: /*
1341: * Apply an madvise behavior to a region of a vma. madvise_update_vma
1342: * will handle splitting a vm area into separate areas, each area with its own
1343: * behavior.
1344: */
1345: static int madvise_vma_behavior(struct madvise_behavior *madv_behavior)
1346: {
1347: int behavior = madv_behavior->behavior;
1348: struct vm_area_struct *vma = madv_behavior->vma;
1349: vm_flags_t new_flags = vma->vm_flags;
1350: struct madvise_behavior_range *range = &madv_behavior->range;
1351: int error;
1352:
1353: if (unlikely(!can_madvise_modify(madv_behavior)))
1354: return -EPERM;
1355:
1356: switch (behavior) {
1357: case MADV_REMOVE:
1358: return madvise_remove(madv_behavior);
1359: case MADV_WILLNEED:
1360: return madvise_willneed(madv_behavior);
1361: case MADV_COLD:
1362: return madvise_cold(madv_behavior);
1363: case MADV_PAGEOUT:
1364: return madvise_pageout(madv_behavior);
1365: case MADV_FREE:
1366: case MADV_DONTNEED:
1367: case MADV_DONTNEED_LOCKED:
1368: return madvise_dontneed_free(madv_behavior);
1369: case MADV_COLLAPSE:
1370: return madvise_collapse(vma, range->start, range->end,
1371: &madv_behavior->lock_dropped);
1372: case MADV_GUARD_INSTALL:
1373: return madvise_guard_install(madv_behavior);
1374: case MADV_GUARD_REMOVE:
1375: return madvise_guard_remove(madv_behavior);
1376:
1377: /* The below behaviours update VMAs via madvise_update_vma(). */
1378:
1379: case MADV_NORMAL:
1380: new_flags = new_flags & ~VM_RAND_READ & ~VM_SEQ_READ;
1381: break;
1382: case MADV_SEQUENTIAL:
1383: new_flags = (new_flags & ~VM_RAND_READ) | VM_SEQ_READ;
1384: break;
1385: case MADV_RANDOM:
1386: new_flags = (new_flags & ~VM_SEQ_READ) | VM_RAND_READ;
1387: break;
1388: case MADV_DONTFORK:
1389: new_flags |= VM_DONTCOPY;
1390: break;
1391: case MADV_DOFORK:
1392: if (new_flags & VM_IO)
1393: return -EINVAL;
1394: new_flags &= ~VM_DONTCOPY;
1395: break;
1396: case MADV_WIPEONFORK:
1397: /* MADV_WIPEONFORK is only supported on anonymous memory. */
1398: if (vma->vm_file || new_flags & VM_SHARED)
1399: return -EINVAL;
1400: new_flags |= VM_WIPEONFORK;
1401: break;
1402: case MADV_KEEPONFORK:
1403: if (new_flags & VM_DROPPABLE)
1404: return -EINVAL;
1405: new_flags &= ~VM_WIPEONFORK;
1406: break;
1407: case MADV_DONTDUMP:
1408: new_flags |= VM_DONTDUMP;
1409: break;
1410: case MADV_DODUMP:
1411: if ((!is_vm_hugetlb_page(vma) && (new_flags & VM_SPECIAL)) ||
1412: (new_flags & VM_DROPPABLE))
1413: return -EINVAL;
1414: new_flags &= ~VM_DONTDUMP;
1415: break;
1416: case MADV_MERGEABLE:
1417: case MADV_UNMERGEABLE:
1418: error = ksm_madvise(vma, range->start, range->end,
1419: behavior, &new_flags);
1420: if (error)
1421: goto out;
1422: break;
1423: case MADV_HUGEPAGE:
1424: case MADV_NOHUGEPAGE:
1425: error = hugepage_madvise(vma, &new_flags, behavior);
1426: if (error)
1427: goto out;
1428: break;
1429: case __MADV_SET_ANON_VMA_NAME:
1430: /* Only anonymous mappings can be named */
1431: if (vma->vm_file && !vma_is_anon_shmem(vma))
1432: return -EBADF;
1433: break;
1434: }
1435:
1436: /* This is a write operation.*/
1437: VM_WARN_ON_ONCE(madv_behavior->lock_mode != MADVISE_MMAP_WRITE_LOCK);
1438:
1439: error = madvise_update_vma(new_flags, madv_behavior);
1440: out:
1441: /*
1442: * madvise() returns EAGAIN if kernel resources, such as
1443: * slab, are temporarily unavailable.
1444: */
1445: if (error == -ENOMEM)
1446: error = -EAGAIN;
1447: return error;
1448: }
SourceFile:mm/madvise.c]
|
| 126/2 |
2026/06/02 16:22 |
llm |
expert |
0m
Model:
Tokens:
input: 94429
output: 39
thoughts: 24
Thoughts: Okay, here's my take on those thoughts, framed as an expert's internal monologue:
**Diving into `madvise_dontneed_free`**
Alright, time to dig into this. I've got to refresh my memory on the intricacies of `madvise_dontneed_free` – let's jump straight into `mm/madvise.c`. I know this is where the core logic of `madvise` and its various flags resides, including the interactions with page reclamation and memory management in general. I need to understand how the "don't need" hint is translated into actual kernel actions when `free` also comes into play. I'm particularly interested in how the kernel handles the potential for races between these calls and memory reuse. It's that dance between hinting, eviction, and availability that's the core of the challenge here. Let's see how the implementation tackles the coordination of these operations and what kind of guarantees, if any, it provides regarding data integrity and potential for performance impacts when handling that flag. Let's see if this is an area where we can fine-tune things or discover some underlying assumptions.
|
| 127/2 |
2026/06/02 16:22 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/madvise.c Name:madvise_dontneed_free]
Results: map[SourceCode: 902: static long madvise_dontneed_free(struct madvise_behavior *madv_behavior)
903: {
904: struct mm_struct *mm = madv_behavior->mm;
905: struct madvise_behavior_range *range = &madv_behavior->range;
906: int behavior = madv_behavior->behavior;
907:
908: if (!madvise_dontneed_free_valid_vma(madv_behavior))
909: return -EINVAL;
910:
911: if (range->start == range->end)
912: return 0;
913:
914: if (!userfaultfd_remove(madv_behavior->vma, range->start, range->end)) {
915: struct vm_area_struct *vma;
916:
917: mark_mmap_lock_dropped(madv_behavior);
918: mmap_read_lock(mm);
919: madv_behavior->vma = vma = vma_lookup(mm, range->start);
920: if (!vma)
921: return -ENOMEM;
922: /*
923: * Potential end adjustment for hugetlb vma is OK as
924: * the check below keeps end within vma.
925: */
926: if (!madvise_dontneed_free_valid_vma(madv_behavior))
927: return -EINVAL;
928: if (range->end > vma->vm_end) {
929: /*
930: * Don't fail if end > vma->vm_end. If the old
931: * vma was split while the mmap_lock was
932: * released the effect of the concurrent
933: * operation may not cause madvise() to
934: * have an undefined result. There may be an
935: * adjacent next vma that we'll walk
936: * next. userfaultfd_remove() will generate an
937: * UFFD_EVENT_REMOVE repetition on the
938: * end-vma->vm_end range, but the manager can
939: * handle a repetition fine.
940: */
941: range->end = vma->vm_end;
942: }
943: /*
944: * If the memory region between start and end was
945: * originally backed by 4kB pages and then remapped to
946: * be backed by hugepages while mmap_lock was dropped,
947: * the adjustment for hugetlb vma above may have rounded
948: * end down to the start address.
949: */
950: if (range->start == range->end)
951: return 0;
952: VM_WARN_ON(range->start > range->end);
953: }
954:
955: if (behavior == MADV_DONTNEED || behavior == MADV_DONTNEED_LOCKED)
956: return madvise_dontneed_single_vma(madv_behavior);
957: else if (behavior == MADV_FREE)
958: return madvise_free_single_vma(madv_behavior);
959: else
960: return -EINVAL;
961: }
SourceFile:mm/madvise.c]
|
| 128/2 |
2026/06/02 16:22 |
llm |
expert |
0m
Model:
Tokens:
input: 95732
output: 44
thoughts: 19
Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience, presented in the first person:
**Deep Dive into `madvise_dontneed_free_valid_vma`**
Alright, let's get down to brass tacks. My initial thought is to zero in on `madvise_dontneed_free_valid_vma`. That function name itself is a mouthful, but it hints at a crucial area of memory management, specifically concerning the interaction of `madvise(MADV_DONTNEED)` with VMAs (Virtual Memory Areas) and the potential for freeing memory, even when a VMA appears valid. I need to unpack what's really happening.
My brain's firing off questions immediately. Is this about reclaiming pages? Is it just marking them as reclaimable, or is there an actual immediate freeing action involved? And what constitutes a "valid" VMA in this context? Does it mean the VMA is mapped and present, or is there a different definition we're using internally? I need to understand the nuances of the VMA flags and their interaction with `MADV_DONTNEED`.
I'm thinking about potential implications. Are there race conditions I need to consider? Can this lead to data loss if not handled carefully? And what about the performance implications? Is this a synchronous or asynchronous operation? If it's asynchronous, how are the freed pages tracked, and what guarantees are there about their eventual reuse or release? I know this area can get intricate quickly, so I need to make sure I'm taking the right approach.
Finally, I'm thinking about the kernel source code. I know this specific area of memory management very well, so I'm already imagining the kernel data structures that are probably involved. I need to map my knowledge of these structures to this function and start tracing the code execution path. I'm going to follow how the VMAs are being managed, how the pages are being handled, and what checks are performed to ensure everything is done safely. This requires close attention to the details of page table management and how memory is allocated and deallocated.
|
| 129/2 |
2026/06/02 16:22 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/madvise.c Name:madvise_dontneed_free_valid_vma]
Results: map[SourceCode: 870: static
871: bool madvise_dontneed_free_valid_vma(struct madvise_behavior *madv_behavior)
872: {
873: struct vm_area_struct *vma = madv_behavior->vma;
874: int behavior = madv_behavior->behavior;
875: struct madvise_behavior_range *range = &madv_behavior->range;
876:
877: if (!is_vm_hugetlb_page(vma)) {
878: unsigned int forbidden = VM_PFNMAP;
879:
880: if (behavior != MADV_DONTNEED_LOCKED)
881: forbidden |= VM_LOCKED;
882:
883: return !(vma->vm_flags & forbidden);
884: }
885:
886: if (behavior != MADV_DONTNEED && behavior != MADV_DONTNEED_LOCKED)
887: return false;
888: if (range->start & ~huge_page_mask(hstate_vma(vma)))
889: return false;
890:
891: /*
892: * Madvise callers expect the length to be rounded up to PAGE_SIZE
893: * boundaries, and may be unaware that this VMA uses huge pages.
894: * Avoid unexpected data loss by rounding down the number of
895: * huge pages freed.
896: */
897: range->end = ALIGN_DOWN(range->end, huge_page_size(hstate_vma(vma)));
898:
899: return true;
900: }
SourceFile:mm/madvise.c]
|
| 130/2 |
2026/06/02 16:22 |
llm |
expert |
0m
Model:
Tokens:
input: 96748
output: 41
thoughts: 63
Thoughts: Okay, here's my interpretation of those thoughts, tailored for an expert audience, as if they were my own:
**Digging into the `MADV_FREE` Implementation**
Hmm, hold on a second... `madvise_dontneed_free_valid_vma`. That code path isn't specifically checking for *anonymous* VMAs, is it? That's a bit surprising. I always assumed that `MADV_FREE` was strictly for anonymous memory, effectively a way to mark pages as discardable when they aren't backed by a file. If it doesn't check for anonymity, then... does that imply that `MADV_FREE` *can* actually be called on file-backed VMAs? That opens up a whole new set of possibilities (and potential complexities) that I hadn't fully considered.
Right, let's dive into `madvise_free_single_vma` and clarify this. I need to understand exactly what happens to those file-backed pages when this `MADV_FREE` path is taken. Is it just an unmap, or are we going through some kind of more involved file-cache invalidation or page reclaim process? I want to know if it's safe and if so, how.
|
| 131/2 |
2026/06/02 16:22 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/madvise.c Name:madvise_free_single_vma]
Results: map[SourceCode: 798: static int madvise_free_single_vma(struct madvise_behavior *madv_behavior)
799: {
800: struct mm_struct *mm = madv_behavior->mm;
801: struct vm_area_struct *vma = madv_behavior->vma;
802: unsigned long start_addr = madv_behavior->range.start;
803: unsigned long end_addr = madv_behavior->range.end;
804: struct mmu_notifier_range range;
805: struct mmu_gather *tlb = madv_behavior->tlb;
806: struct mm_walk_ops walk_ops = {
807: .pmd_entry = madvise_free_pte_range,
808: };
809:
810: /* MADV_FREE works for only anon vma at the moment */
811: if (!vma_is_anonymous(vma))
812: return -EINVAL;
813:
814: range.start = max(vma->vm_start, start_addr);
815: if (range.start >= vma->vm_end)
816: return -EINVAL;
817: range.end = min(vma->vm_end, end_addr);
818: if (range.end <= vma->vm_start)
819: return -EINVAL;
820: mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm,
821: range.start, range.end);
822:
823: lru_add_drain();
824: update_hiwater_rss(mm);
825:
826: mmu_notifier_invalidate_range_start(&range);
827: tlb_start_vma(tlb, vma);
828: walk_ops.walk_lock = get_walk_lock(madv_behavior->lock_mode);
829: walk_page_range_vma(vma, range.start, range.end,
830: &walk_ops, tlb);
831: tlb_end_vma(tlb, vma);
832: mmu_notifier_invalidate_range_end(&range);
833: return 0;
834: }
SourceFile:mm/madvise.c]
|
| 132/2 |
2026/06/02 16:22 |
llm |
expert |
0m
Model:
Tokens:
input: 97811
output: 34
thoughts: 3081
Thoughts: **Deep Dive into Memory Accounting: Tracking Down a Persistent Counter Discrepancy**
Okay, so I'm chasing a strange issue where `MM_FILEPAGES` is showing a value of 1 and `MM_SWAPENTS` is 32. This seems to be happening after a series of operations in memory management, so let's carefully walk through the likely candidates.
First, let's establish some fundamentals. `MADV_FREE` targets anonymous VMAs, meaning `madvise_free_pte_range` deals exclusively with anon pages. This makes the `MM_SWAPENTS` counter changes within the `MADV_FREE` path sound.
Next, I'm reconsidering scenarios where we modify file-backed memory. I'm focusing on the interaction between `uprobe` and the memory accounting counters. A `uprobe` on a file VMA replaces a file folio with an anonymous folio, which should decrement `MM_FILEPAGES`, increment `MM_ANONPAGES`, and if swapped, increment `MM_SWAPENTS`. Unregistering `uprobe` seems trickier. `__uprobe_write_opcode` decrements `MM_ANONPAGES` and unmaps the anon folio, potentially leading to the file folio being re-faulted in. The crucial point here is whether `__uprobe_write_opcode` *also* increments `MM_FILEPAGES`. It *doesn't*! It simply leaves the page as NONE, and `MM_FILEPAGES` should only increment when the file folio is re-faulted. So, the old scheme where `__replace_page` decremented `MM_FILEPAGES` when registering, but not incrementing when unregistering, is perfectly balanced.
Could a bug in `mremap` be the culprit? If `mremap` with `MREMAP_DONTUNMAP` moves the PTEs, that doesn't change `MM_FILEPAGES` or `MM_SWAPENTS`. The important point is that the PTEs of the "old" memory region become NONE, which doesn't disrupt counter balance.
What about `fork`? During `fork`, PTEs are copied, incrementing `MM_ANONPAGES` for anonymous folios, `MM_FILEPAGES` for file folios, and `MM_SWAPENTS` for swap entries. The RSS calculations are correctly implemented during this copy. I'm confident `fork` handles this correctly.
If `khugepaged` collapses a file THP, the counters are correctly adjusted for the change from 32 individual pages to one THP. If an anon THP is collapsed, swap entries are handled correctly in reducing `MM_SWAPENTS`. Even shared memory swap entries shouldn't be involved in the anon THP collapse scenario, so that's covered.
I'm revisiting the `uprobe` on shared zeropage scenario. The commit `bddf10d26e6e` correctly fixes a bug where `MM_FILEPAGES` was incorrectly decremented. However, that patch would result in `MM_FILEPAGES` going negative, and my report shows a value of 1. So that does not explain the issue! Perhaps there's a bug during a failed `uprobe` registration after an initial replacement? When a registration fails, the counters get correctly adjusted, and the zeropage, if replaced is put back in place. So again, that does not explain the issue.
I'm now hunting for *incorrect* increments of `MM_FILEPAGES`. Or, is the page leaked? I need to think about situations where the counters are incremented but the corresponding page is not released. Detached page tables? Skipped PTEs during unmapping? `zap_pte_range` is my focus here. This is potentially interesting. If a PTE is skipped during unmapping (if it's `pte_none` when it *shouldn't* be), then that *could* lead to a leak. Could a PMD be cleared without clearing the PTEs? That could happen during a `khugepaged` operation. It would be a significant issue if `khugepaged` fails *after* clearing the PMD but *before* unmapping the associated PTEs. Checking `__collapse_huge_page_isolate` or `collapse_huge_page`, the PMD is restored on a failure. That means no leak.
Concurrent access, such as between `madvise_free_pte_range` and `khugepaged`, should be synchronized.
Let's quickly consider `userfaultfd` and `UFFDIO_MOVE`. The counters appear to remain stable even when PMDs are moved during `move_pages_pmd`. So
|
| 133/2 |
2026/06/02 16:23 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/memory.c Name:swap_pte_batch]
Results: map[SourceCode: 434: /**
435: * swap_pte_batch - detect a PTE batch for a set of contiguous swap entries
436: * @start_ptep: Page table pointer for the first entry.
437: * @max_nr: The maximum number of table entries to consider.
438: * @pte: Page table entry for the first entry.
439: *
440: * Detect a batch of contiguous swap entries: consecutive (non-present) PTEs
441: * containing swap entries all with consecutive offsets and targeting the same
442: * swap type, all with matching swp pte bits.
443: *
444: * max_nr must be at least one and must be limited by the caller so scanning
445: * cannot exceed a single page table.
446: *
447: * Return: the number of table entries in the batch.
448: */
449: static inline int swap_pte_batch(pte_t *start_ptep, int max_nr, pte_t pte)
450: {
451: pte_t expected_pte = pte_next_swp_offset(pte);
452: const pte_t *end_ptep = start_ptep + max_nr;
453: const softleaf_t entry = softleaf_from_pte(pte);
454: pte_t *ptep = start_ptep + 1;
455: unsigned short cgroup_id;
456:
457: VM_WARN_ON(max_nr < 1);
458: VM_WARN_ON(!softleaf_is_swap(entry));
459:
460: cgroup_id = lookup_swap_cgroup_id(entry);
461: while (ptep < end_ptep) {
462: softleaf_t entry;
463:
464: pte = ptep_get(ptep);
465:
466: if (!pte_same(pte, expected_pte))
467: break;
468: entry = softleaf_from_pte(pte);
469: if (lookup_swap_cgroup_id(entry) != cgroup_id)
470: break;
471: expected_pte = pte_next_swp_offset(expected_pte);
472: ptep++;
473: }
474:
475: return ptep - start_ptep;
476: }
SourceFile:mm/internal.h]
|
| 134/2 |
2026/06/02 16:23 |
llm |
expert |
0m
Model:
Tokens:
input: 102684
output: 39
thoughts: 4735
Thoughts: **Analyzing Memory Accounting in the Kernel**
Okay, let's break this down systematically. I'm focusing on swap and file page accounting, especially in relation to mTHPs and `uprobe`. First, the `swap_pte_batch` function checks for contiguous swap entries, and based on what I see here, it seems to work correctly: a batch of pages is freed, and the `MM_SWAPENTS` counter is decremented accordingly. The potential for a mismatch appears unlikely, and the accounting for batched and single-page swap-outs seems sound.
I'm following the logic through `try_to_unmap_one` and noting that it, crucially, unmaps pages individually, incrementing `MM_SWAPENTS` by one for each page, even for mTHPs. Then, when `do_swap_page` maps a large page, the code seems to correctly adjust `MM_SWAPENTS` based on the page count; decrementing, that is, when swapping *in*. The handling of failures during `do_swap_page` appears safe, ensuring counters are consistent even with errors. I keep returning to `swap_pte_batch`, which checks multiple pte's for contiguous swap entries.
Then, the scenario where swap_pte_batch returns a number less than the total folio_nr_pages(folio) shows me where a single page gets mapped, with the rest potentially remaining swapped out, which seems correct. Importantly, it checks whether the page it's mapping is an anonymous page (`folio_test_anon`), meaning file-backed pages would go down a different code path. The accounting within that logic is also correct, decrementing `MM_SWAPENTS` by 1. The code makes it so the page table entry is *consistent* to ensure the counter is the same.
The potential for subtle errors in `swap_pte_batch` itself is slim because it seems to be working correctly with ensuring the contiguous swap entries.
Now, focusing on the combination of `uprobe` and file-backed pages. It first hit me that the issue could arise in the code related to `uprobe`, when a file-backed page could be swapped out. The analysis points out that `get_user_pages_remote` with `FOLL_WRITE` triggers a COW fault. The initial page will be decremented by one in the MM_FILEPAGES counter, and incremented by one in MM_ANONPAGES, meaning an anon page will be created.
When unregistering `uprobe` (with a write), I realize that the same is true. Since the `MM_FILEPAGES` is *NOT* incremented, and `MM_ANONPAGES` is *NOT* decremented, this must be where the bug is. The code does NOT update the counter correctly in the unregistering path when unregistering. However, this is because a file page is not affected by this write, but rather the anon page, so this seems to be fine.
But if it's the zero page... then I recall `uprobe_write_opcode` explicitly aborts if the folio isn't anon. This is good: a check to keep it safe.
So then what is the source of the issue? Looking at userfaultfd, I see that the `mfill_atomic` family of functions are correctly accounting for anonymous and zero pages. But the shmem page cache seems to be incrementing the shared memory page counters. It *seems* like the zero page counter isn't incremented. But what happens if something other than the zero page is written to, such as a file page, or even a shared memory page?
|
| 135/2 |
2026/06/02 16:23 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/shmem.c Name:shmem_mfill_atomic_pte]
Results: map[SourceCode:3184: int shmem_mfill_atomic_pte(pmd_t *dst_pmd,
3185: struct vm_area_struct *dst_vma,
3186: unsigned long dst_addr,
3187: unsigned long src_addr,
3188: uffd_flags_t flags,
3189: struct folio **foliop)
3190: {
3191: struct inode *inode = file_inode(dst_vma->vm_file);
3192: struct shmem_inode_info *info = SHMEM_I(inode);
3193: struct address_space *mapping = inode->i_mapping;
3194: gfp_t gfp = mapping_gfp_mask(mapping);
3195: pgoff_t pgoff = linear_page_index(dst_vma, dst_addr);
3196: void *page_kaddr;
3197: struct folio *folio;
3198: int ret;
3199: pgoff_t max_off;
3200:
3201: if (shmem_inode_acct_blocks(inode, 1)) {
3202: /*
3203: * We may have got a page, returned -ENOENT triggering a retry,
3204: * and now we find ourselves with -ENOMEM. Release the page, to
3205: * avoid a BUG_ON in our caller.
3206: */
3207: if (unlikely(*foliop)) {
3208: folio_put(*foliop);
3209: *foliop = NULL;
3210: }
3211: return -ENOMEM;
3212: }
3213:
3214: if (!*foliop) {
3215: ret = -ENOMEM;
3216: folio = shmem_alloc_folio(gfp, 0, info, pgoff);
3217: if (!folio)
3218: goto out_unacct_blocks;
3219:
3220: if (uffd_flags_mode_is(flags, MFILL_ATOMIC_COPY)) {
3221: page_kaddr = kmap_local_folio(folio, 0);
3222: /*
3223: * The read mmap_lock is held here. Despite the
3224: * mmap_lock being read recursive a deadlock is still
3225: * possible if a writer has taken a lock. For example:
3226: *
3227: * process A thread 1 takes read lock on own mmap_lock
3228: * process A thread 2 calls mmap, blocks taking write lock
3229: * process B thread 1 takes page fault, read lock on own mmap lock
3230: * process B thread 2 calls mmap, blocks taking write lock
3231: * process A thread 1 blocks taking read lock on process B
3232: * process B thread 1 blocks taking read lock on process A
3233: *
3234: * Disable page faults to prevent potential deadlock
3235: * and retry the copy outside the mmap_lock.
3236: */
3237: pagefault_disable();
3238: ret = copy_from_user(page_kaddr,
3239: (const void __user *)src_addr,
3240: PAGE_SIZE);
3241: pagefault_enable();
3242: kunmap_local(page_kaddr);
3243:
3244: /* fallback to copy_from_user outside mmap_lock */
3245: if (unlikely(ret)) {
3246: *foliop = folio;
3247: ret = -ENOENT;
3248: /* don't free the page */
3249: goto out_unacct_blocks;
3250: }
3251:
3252: flush_dcache_folio(folio);
3253: } else { /* ZEROPAGE */
3254: clear_user_highpage(&folio->page, dst_addr);
3255: }
3256: } else {
3257: folio = *foliop;
3258: VM_BUG_ON_FOLIO(folio_test_large(folio), folio);
3259: *foliop = NULL;
3260: }
3261:
3262: VM_BUG_ON(folio_test_locked(folio));
3263: VM_BUG_ON(folio_test_swapbacked(folio));
3264: __folio_set_locked(folio);
3265: __folio_set_swapbacked(folio);
3266: __folio_mark_uptodate(folio);
3267:
3268: ret = -EFAULT;
3269: max_off = DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE);
3270: if (unlikely(pgoff >= max_off))
3271: goto out_release;
3272:
3273: ret = mem_cgroup_charge(folio, dst_vma->vm_mm, gfp);
3274: if (ret)
3275: goto out_release;
3276: ret = shmem_add_to_page_cache(folio, mapping, pgoff, NULL, gfp);
3277: if (ret)
3278: goto out_release;
3279:
3280: ret = mfill_atomic_install_pte(dst_pmd, dst_vma, dst_addr,
3281: &folio->page, true, flags);
3282: if (ret)
3283: goto out_delete_from_cache;
3284:
3285: shmem_recalc_inode(inode, 1, 0);
3286: folio_unlock(folio);
3287: return 0;
3288: out_delete_from_cache:
3289: filemap_remove_folio(folio);
3290: out_release:
3291: folio_unlock(folio);
3292: folio_put(folio);
3293: out_unacct_blocks:
3294: shmem_inode_unacct_blocks(inode, 1);
3295: return ret;
3296: }
SourceFile:mm/shmem.c]
|
| 136/2 |
2026/06/02 16:23 |
llm |
expert |
0m
Model:
Tokens:
input: 110044
output: 39
thoughts: 27
Thoughts: Okay, here's my take on those thoughts, framed as an expert's internal monologue:
**Diving into the PTE Installation**
Alright, the code's calling `mfill_atomic_install_pte`. Good, good. That's where the rubber meets the road, the core of the mmap'd file mapping process. Let me quickly refresh my understanding of *that* function... Okay, right, `mfill_atomic_install_pte`. I need to zero in on how it's handling the atomic updates – the crux of concurrent access management. I'm going to quickly check what its doing, and what it depends on. I bet it uses some spinlocks and RCU to get this right. I wonder if there are any subtle race conditions I've missed. Let's see what the latest implementation does.
|
| 137/2 |
2026/06/02 16:23 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/userfaultfd.c Name:mfill_atomic_install_pte]
Results: map[SourceCode: 162: /*
163: * Install PTEs, to map dst_addr (within dst_vma) to page.
164: *
165: * This function handles both MCOPY_ATOMIC_NORMAL and _CONTINUE for both shmem
166: * and anon, and for both shared and private VMAs.
167: */
168: int mfill_atomic_install_pte(pmd_t *dst_pmd,
169: struct vm_area_struct *dst_vma,
170: unsigned long dst_addr, struct page *page,
171: bool newly_allocated, uffd_flags_t flags)
172: {
173: int ret;
174: struct mm_struct *dst_mm = dst_vma->vm_mm;
175: pte_t _dst_pte, *dst_pte;
176: bool writable = dst_vma->vm_flags & VM_WRITE;
177: bool vm_shared = dst_vma->vm_flags & VM_SHARED;
178: spinlock_t *ptl;
179: struct folio *folio = page_folio(page);
180: bool page_in_cache = folio_mapping(folio);
181: pte_t dst_ptep;
182:
183: _dst_pte = mk_pte(page, dst_vma->vm_page_prot);
184: _dst_pte = pte_mkdirty(_dst_pte);
185: if (page_in_cache && !vm_shared)
186: writable = false;
187: if (writable)
188: _dst_pte = pte_mkwrite(_dst_pte, dst_vma);
189: if (flags & MFILL_ATOMIC_WP)
190: _dst_pte = pte_mkuffd_wp(_dst_pte);
191:
192: ret = -EAGAIN;
193: dst_pte = pte_offset_map_lock(dst_mm, dst_pmd, dst_addr, &ptl);
194: if (!dst_pte)
195: goto out;
196:
197: if (mfill_file_over_size(dst_vma, dst_addr)) {
198: ret = -EFAULT;
199: goto out_unlock;
200: }
201:
202: ret = -EEXIST;
203:
204: dst_ptep = ptep_get(dst_pte);
205:
206: /*
207: * We are allowed to overwrite a UFFD pte marker: consider when both
208: * MISSING|WP registered, we firstly wr-protect a none pte which has no
209: * page cache page backing it, then access the page.
210: */
211: if (!pte_none(dst_ptep) && !pte_is_uffd_marker(dst_ptep))
212: goto out_unlock;
213:
214: if (page_in_cache) {
215: /* Usually, cache pages are already added to LRU */
216: if (newly_allocated)
217: folio_add_lru(folio);
218: folio_add_file_rmap_pte(folio, page, dst_vma);
219: } else {
220: folio_add_new_anon_rmap(folio, dst_vma, dst_addr, RMAP_EXCLUSIVE);
221: folio_add_lru_vma(folio, dst_vma);
222: }
223:
224: /*
225: * Must happen after rmap, as mm_counter() checks mapping (via
226: * PageAnon()), which is set by __page_set_anon_rmap().
227: */
228: inc_mm_counter(dst_mm, mm_counter(folio));
229:
230: set_pte_at(dst_mm, dst_addr, dst_pte, _dst_pte);
231:
232: /* No need to invalidate - it was non-present before */
233: update_mmu_cache(dst_vma, dst_addr, dst_pte);
234: ret = 0;
235: out_unlock:
236: pte_unmap_unlock(dst_pte, ptl);
237: out:
238: return ret;
239: }
SourceFile:mm/userfaultfd.c]
|
| 138/2 |
2026/06/02 16:23 |
llm |
expert |
0m
Model:
Tokens:
input: 111616
output: 37
thoughts: 4100
Thoughts: **Analysis of Page Counter and UFFD Interaction**
Okay, let's break this down. My initial thought process revolves around understanding how page counters, specifically `MM_FILEPAGES` and `MM_SWAPENTS`, are incremented and decremented in relation to various memory management operations. The core issue seems to be a leak, as indicated by a report of `MM_FILEPAGES val:1` and `MM_SWAPENTS val:32` in the same crash.
My initial hypothesis centered on file pages: Could a file VMA leak file pages, leading to this imbalance? I considered scenarios involving `fork`, `uprobe` (and its unregistration), and the operation of `khugepaged` with the zero page, as well as the use of `madvise_dontneed_free` and `mremap`. I examined the interactions of those mechanisms with file-backed VMAs. The conclusion was that, on their own, those mechanisms were not the cause of the double-leak; none directly incremented `MM_SWAPENTS` for a file VMA. It also became clear that while `khugepaged` can collapse anon or shmem THPs, and the zero page is never actually accounted for in `MM_FILEPAGES`.
However, the key insight emerged with the introduction of `userfaultfd` (`UFFD`) and specifically, the reproducer given in the bug report using `UFFDIO_ZEROPAGE`. The reproducer's use of `mmap` with a file (`./file0`), combined with `UFFDIO_ZEROPAGE`, pointed to a potentially critical interaction.
I realized that `UFFDIO_ZEROPAGE` seemed to be operating on a file-backed VMA. This seemed impossible due to the checks in `mfill_atomic` preventing direct interaction with file-backed VMAs. Then I realized the clue: this file was likely a tmpfs file. If the file `./file0` resides on a tmpfs (shmem) file system, the vma could be shmem and that would be consistent with the reproducer's operation and the crash.
The critical piece here is the use of `mfill_atomic_pte_zeropage`, which is not constrained by `vma_is_shmem`. When combined with the bug report information, this scenario suggested that `UFFDIO_ZEROPAGE` was mismanaging the accounting. In particular, the ZEROPAGE pte was being set without incrementing the RSS.
Therefore, the key lies in an issue where the `mfill_atomic_pte` is called without appropriate counter management, leading to the leak of `MM_FILEPAGES` and `MM_SWAPENTS`.
|
| 139/2 |
2026/06/02 16:24 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/userfaultfd.c Name:mfill_atomic_pte]
Results: map[SourceCode: 660: static __always_inline ssize_t mfill_atomic_pte(pmd_t *dst_pmd,
661: struct vm_area_struct *dst_vma,
662: unsigned long dst_addr,
663: unsigned long src_addr,
664: uffd_flags_t flags,
665: struct folio **foliop)
666: {
667: ssize_t err;
668:
669: if (uffd_flags_mode_is(flags, MFILL_ATOMIC_CONTINUE)) {
670: return mfill_atomic_pte_continue(dst_pmd, dst_vma,
671: dst_addr, flags);
672: } else if (uffd_flags_mode_is(flags, MFILL_ATOMIC_POISON)) {
673: return mfill_atomic_pte_poison(dst_pmd, dst_vma,
674: dst_addr, flags);
675: }
676:
677: /*
678: * The normal page fault path for a shmem will invoke the
679: * fault, fill the hole in the file and COW it right away. The
680: * result generates plain anonymous memory. So when we are
681: * asked to fill an hole in a MAP_PRIVATE shmem mapping, we'll
682: * generate anonymous memory directly without actually filling
683: * the hole. For the MAP_PRIVATE case the robustness check
684: * only happens in the pagetable (to verify it's still none)
685: * and not in the radix tree.
686: */
687: if (!(dst_vma->vm_flags & VM_SHARED)) {
688: if (uffd_flags_mode_is(flags, MFILL_ATOMIC_COPY))
689: err = mfill_atomic_pte_copy(dst_pmd, dst_vma,
690: dst_addr, src_addr,
691: flags, foliop);
692: else
693: err = mfill_atomic_pte_zeropage(dst_pmd,
694: dst_vma, dst_addr);
695: } else {
696: err = shmem_mfill_atomic_pte(dst_pmd, dst_vma,
697: dst_addr, src_addr,
698: flags, foliop);
699: }
700:
701: return err;
702: }
SourceFile:mm/userfaultfd.c]
|
| 140/2 |
2026/06/02 16:24 |
llm |
expert |
0m
Model:
Tokens:
input: 117087
output: 38
thoughts: 6913
Thoughts: **Analyzing a Suspected RSS Counter Bug in the Linux Kernel**
Okay, so I've been staring at this code and bug report for a while now, trying to piece together what's going on with these RSS counters. We've got this `MM_FILEPAGES` value showing as 1 and `MM_SWAPENTS` showing as 32, and it seems like a file-backed page is somehow being accounted incorrectly, along with some leaked swap entries. I've been running through all the potential scenarios in my head, focusing on how pages are mapped, unmapped, and swapped, and what functions interact with those counters.
First, I looked at shmem mappings because the code snippet involved shared memory. But that seems unlikely because it has its own logic for handling pages. Then I remembered a past bug, where there was an issue with a private shmem mapping, and the call to `mfill_atomic_pte_zeropage`. But that function maps the zero page and doesn't affect page counters.
Then I considered `uprobe`. Since `uprobe` can trace file-backed pages, which could involve splitting THPs, and then it replaces a single page. If `uprobe` replaced one file page and a huge page was involved, it might cause some problems with accounting.
The other obvious area to consider is swap entry management. If a swap entry is present in a PTE, and then the PTE is removed, the associated swap entry has to be handled correctly. I went over `madvise_free`, `MADV_DONTNEED`, and `do_swap_page` and verified that they handled the counter increments and decrements correctly. The bug could potentially happen if `MM_SWAPENTS` is incremented but not decremented. I ruled out this possibility.
Then, I looked at a more obscure possibility, userfaultfd. I tried to figure out if it could map a page, but it seems to correctly manage counters too.
I started to dig deeper into potential race conditions, considering that `percpu_counter` is used. But I keep coming back to the fact that the bug is reported consistently by syzbot, so I doubt it's just a simple race.
Then I focused on `unmap_mapping_pages`. The relevant part is `zap_pte_range` and I considered the scenario in which the latter unmaps page-cache backed pages, and leaves anon pages intact. Could the problem be related to an mTHP? I tried to work out if this could result in leaked swap entries, but that seems unlikely, and I verified that the code should correctly decrement `MM_SWAPENTS`.
I started to get really concerned when thinking about memory corruption. I've seen that before, and it can cause unpredictable values, but it's hard to find in a systematic way.
But then, I had a breakthrough! The fact that we have `MM_FILEPAGES val: 1` and `MM_SWAPENTS val: 32` suggests that something is *clearing* a PMD (Page Map Directory) table entry. If a PMD is cleared, it can unmap all PTEs in the range. It could contain one file page and 32 swap entries. If `zap_pte_range` is called on that PMD, but doesn't handle the swap entries, the counter values would line up perfectly. This is the smoking gun!
I checked all the ways a PMD can be cleared, especially during huge page operations. The key functions are `pmdp_collapse_flush` and functions like `retract_page_tables`. And it looks like the culprit is in `retract_page_tables`, which is used by `khugepaged` to collapse file THPs. This code *can* clear the PMD and free the page table. If something goes wrong during that process, if the PTEs are not correctly handled, the swap entries would be leaked, and the file page counter might be off by one! The bug is in `retract_page_tables`!
|
| 141/2 |
2026/06/02 16:25 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/khugepaged.c Name:retract_page_tables]
Results: map[SourceCode:1741: static void retract_page_tables(struct address_space *mapping, pgoff_t pgoff)
1742: {
1743: struct vm_area_struct *vma;
1744:
1745: i_mmap_lock_read(mapping);
1746: vma_interval_tree_foreach(vma, &mapping->i_mmap, pgoff, pgoff) {
1747: struct mmu_notifier_range range;
1748: struct mm_struct *mm;
1749: unsigned long addr;
1750: pmd_t *pmd, pgt_pmd;
1751: spinlock_t *pml;
1752: spinlock_t *ptl;
1753: bool success = false;
1754:
1755: addr = vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT);
1756: if (addr & ~HPAGE_PMD_MASK ||
1757: vma->vm_end < addr + HPAGE_PMD_SIZE)
1758: continue;
1759:
1760: mm = vma->vm_mm;
1761: if (find_pmd_or_thp_or_none(mm, addr, &pmd) != SCAN_SUCCEED)
1762: continue;
1763:
1764: if (hpage_collapse_test_exit(mm))
1765: continue;
1766:
1767: if (!file_backed_vma_is_retractable(vma))
1768: continue;
1769:
1770: /* PTEs were notified when unmapped; but now for the PMD? */
1771: mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm,
1772: addr, addr + HPAGE_PMD_SIZE);
1773: mmu_notifier_invalidate_range_start(&range);
1774:
1775: pml = pmd_lock(mm, pmd);
1776: /*
1777: * The lock of new_folio is still held, we will be blocked in
1778: * the page fault path, which prevents the pte entries from
1779: * being set again. So even though the old empty PTE page may be
1780: * concurrently freed and a new PTE page is filled into the pmd
1781: * entry, it is still empty and can be removed.
1782: *
1783: * So here we only need to recheck if the state of pmd entry
1784: * still meets our requirements, rather than checking pmd_same()
1785: * like elsewhere.
1786: */
1787: if (check_pmd_state(pmd) != SCAN_SUCCEED)
1788: goto drop_pml;
1789: ptl = pte_lockptr(mm, pmd);
1790: if (ptl != pml)
1791: spin_lock_nested(ptl, SINGLE_DEPTH_NESTING);
1792:
1793: /*
1794: * Huge page lock is still held, so normally the page table must
1795: * remain empty; and we have already skipped anon_vma and
1796: * userfaultfd_wp() vmas. But since the mmap_lock is not held,
1797: * it is still possible for a racing userfaultfd_ioctl() or
1798: * madvise() to have inserted ptes or markers. Now that we hold
1799: * ptlock, repeating the retractable checks protects us from
1800: * races against the prior checks.
1801: */
1802: if (likely(file_backed_vma_is_retractable(vma))) {
1803: pgt_pmd = pmdp_collapse_flush(vma, addr, pmd);
1804: pmdp_get_lockless_sync();
1805: success = true;
1806: }
1807:
1808: if (ptl != pml)
1809: spin_unlock(ptl);
1810: drop_pml:
1811: spin_unlock(pml);
1812:
1813: mmu_notifier_invalidate_range_end(&range);
1814:
1815: if (success) {
1816: mm_dec_nr_ptes(mm);
1817: page_table_check_pte_clear_range(mm, addr, pgt_pmd);
1818: pte_free_defer(mm, pmd_pgtable(pgt_pmd));
1819: }
1820: }
1821: i_mmap_unlock_read(mapping);
1822: }
SourceFile:mm/khugepaged.c]
|
| 142/2 |
2026/06/02 16:25 |
llm |
expert |
0m
Model:
Tokens:
input: 126290
output: 43
thoughts: 400
Thoughts: **My Analysis of `retract_page_tables` and Potential Issues**
Okay, hold on a second. I need to fully grasp what's happening in `retract_page_tables`. Looking at the code, it's pretty clear that it *frees* a page table, but not before assuming it's empty! Specifically, the critical section is:
```c
if (likely(file_backed_vma_is_retractable(vma))) {
pgt_pmd = pmdp_collapse_flush(vma, addr, pmd);
pmdp_get_lockless_sync();
success = true;
}
// ...
if (success) {
mm_dec_nr_ptes(mm);
page_table_check_pte_clear_range(mm, addr, pgt_pmd);
pte_free_defer(mm, pmd_pgtable(pgt_pmd));
}
```
This immediately triggers a red flag – it *frees* the page table without explicitly unmapping the PTEs *inside* it! That's a huge assumption. And it's based on the premise that the page table is *empty*.
Now, the question is *why* it assumes emptiness. The answer lies in the calling code, particularly the `collapse_file` function which precedes the call to `retract_page_tables`. `collapse_file` calls `unmap_mapping_pages` to unmap the file pages:
```c
unmap_mapping_pages(mapping, start, HPAGE_PMD_NR, false);
```
But here's the kicker, `unmap_mapping_pages` has a gotcha! It *skips* swap entries. This happens because it sets `details.even_cows = false` (defaults to false), meaning it *doesn't* unmap COWed pages.
This creates a serious vulnerability if the VMA is `MAP_PRIVATE` and contains COWed anonymous pages. Those pages might be swapped out and represented as swap entries! So, `unmap_mapping_pages` *misses* them. Then, `retract_page_tables` is called, and, critically, `file_backed_vma_is_retractable(vma)` is checked. I need to dig into `file_backed_vma_is_retractable` to see *exactly* what criteria it's using, to understand if this could allow the erroneous page table free to occur!
|
| 143/2 |
2026/06/02 16:25 |
tool |
codesearch-definition-source |
0m
Args: map[ContextFile:mm/khugepaged.c Name:file_backed_vma_is_retractable]
Results: map[SourceCode:1704: /* Can we retract page tables for this file-backed VMA? */
1705: static bool file_backed_vma_is_retractable(struct vm_area_struct *vma)
1706: {
1707: /*
1708: * Check vma->anon_vma to exclude MAP_PRIVATE mappings that
1709: * got written to. These VMAs are likely not worth removing
1710: * page tables from, as PMD-mapping is likely to be split later.
1711: */
1712: if (READ_ONCE(vma->anon_vma))
1713: return false;
1714:
1715: /*
1716: * When a vma is registered with uffd-wp, we cannot recycle
1717: * the page table because there may be pte markers installed.
1718: * Other vmas can still have the same file mapped hugely, but
1719: * skip this one: it will always be mapped in small page size
1720: * for uffd-wp registered ranges.
1721: */
1722: if (userfaultfd_wp(vma))
1723: return false;
1724:
1725: /*
1726: * If the VMA contains guard regions then we can't collapse it.
1727: *
1728: * This is set atomically on guard marker installation under mmap/VMA
1729: * read lock, and here we may not hold any VMA or mmap lock at all.
1730: *
1731: * This is therefore serialised on the PTE page table lock, which is
1732: * obtained on guard region installation after the flag is set, so this
1733: * check being performed under this lock excludes races.
1734: */
1735: if (vma_test_atomic_flag(vma, VMA_MAYBE_GUARD_BIT))
1736: return false;
1737:
1738: return true;
1739: }
SourceFile:mm/khugepaged.c]
|
| 144/2 |
2026/06/02 16:25 |
llm |
expert |
0m
Model:
Tokens:
input: 127885
output: 66
thoughts: 2677
Thoughts: **Analyzing a Kernel Memory Accounting Imbalance**
Okay, so I'm digging into this "Bad rss-counter state" issue, specifically focusing on these `MM_FILEPAGES` and `MM_SWAPENTS` values. First, I see `file_backed_vma_is_retractable` blocks page table retraction if there's any copy-on-write (`anon_vma`) pages. This is expected behavior, preventing leaks of swap entries. But what about cases without an `anon_vma` but *with* swap entries? I start thinking about shared memory (`shmem`) and how it *could* have swap entries, even without an `anon_vma` if it's `MAP_SHARED`. Then I trace the `unmap_mapping_pages` codepath and ensure it is unmapping shmem swap entries. It is! But, it doesn't decrement `MM_SWAPENTS` for them which is why the bug might be occurring.
Next, I consider the possibility of hardware poison entries or UFFD WP markers on the VMA. These checks also prevent retraction if present. But this also shouldn't impact `MM_SWAPENTS` because they're not accounted for.
I move on to consider if there are other ways to have swap entries without `anon_vma`. Not that I can see, at least. Time to rethink the counters! Could `madvise_free_pte_range` on an anon VMA explain things? I'm trying to figure out if there's a file page involved somehow. What about `mremap` with `MREMAP_DONTUNMAP`? No, that also moves VMAs of the same type. Then `userfaultfd_move` is another possibility: can it move file pages into an anon VMA? I check the conditions, and I realize it *cannot* cross anon/file VMA boundaries. But what about the *zero page*? I check `userfaultfd_move`, and it uses `move_pages_pte`. The zero page is a file page! `userfaultfd_move` uses `folio_move_anon_rmap` if the source folio is anonymous. But `move_pages_pte` itself *doesn't* change RSS counters, even for the zero page. So, that's not it, either.
What about `uprobe`? That's an interesting one. Is it possible that `uprobe` somehow corrupts the counters? I go and verify that the zero page is rejected with `uprobe`. And... wait, there was a patch that was added to fix an issue where the zero page was being accepted. Could my report be from *before* that patch? If so, the counter could have been incorrectly decremented by 1. But my report has `MM_FILEPAGES val:1`, not zero or negative! That makes me think something *incremented* the file page counter incorrectly. Can't be from unregistering `uprobe`. Then the counters would be decremented from the anon page.
I am now looking at all code paths that manipulate the `MM_FILEPAGES` counter.
What about `fork` failures and `exit_mmap`? It should unmap everything. But, could it leak a PMD? No.
Now, what is the impact of this bug? I need to analyze its security impact! It's a "Bad rss-counter state" *warning*. This means imbalanced RSS counters in `check_mm`. This could be purely an accounting issue (like with `uprobe`) or a page leak. Crucially, the bug report doesn't show `non-zero pgtables_bytes`. This implies the page tables *were* freed! I'm now certain of it because if they were not freed, it would be a critical issue. So, counters are wrong, but there is no leak. I go back to prior cases and previous commits that state it's nearly always accounting and not a page leak.
Given all of this, this means it's accessible from unprivileged processes (since syzbot triggers it). It's not *exploitable* for privilege escalation. And, since the page table *was* freed, it's not a DoS due to memory leak.
Okay, let me double-check if this can be triggered from a user namespace. Syzbot runs in a user namespace. Yes. No network trigger, no file system trigger, no peripheral trigger, no guest trigger. I'm ready to set the results based on this analysis.
|
| 145/2 |
2026/06/02 16:25 |
tool |
set-results |
0m
Args: map[DenialOfService:false Exploitable:false FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:true UserNamespace:true VMGuestTrigger:false VMHostTrigger:false]
Results: map[DenialOfService:false Exploitable:false FilesystemTrigger:false NetworkTrigger:false PeripheralTrigger:false RemoteTrigger:false Unprivileged:true UserNamespace:true VMGuestTrigger:false VMHostTrigger:false]
|
| 146/2 |
2026/06/02 16:25 |
llm |
expert |
0m
Model:
Tokens:
input: 131668
output: 462
thoughts: 13
Thoughts: Okay, here's the summary, rendered as a first-person thought process, keeping in mind the user is an expert:
**Finalizing the Response**
Alright, the analysis is done. I've considered all the relevant angles, cross-referenced the data, and built a cohesive understanding. Now, it's time to craft the final response. It's not just about regurgitating the findings; it's about presenting them in a way that's insightful and adds value. I need to ensure the framing is sharp, the logic flows, and that it provides the level of nuance this audience expects. No fluff, just direct, actionable takeaways. Let's get this perfectly formulated.
|
| 147/1 |
2026/06/02 16:26 |
action |
format-explanation |
0m
Results: map[Explanation:Based on the analysis of the kernel source code and similar historical bugs,
here is the assessment of the bug:
**Bug Mechanism & Impact:**
The `BUG: Bad rss-counter state` warning is triggered in `check_mm()` during
process termination when the kernel detects that the Resident Set Size (RSS)
counters for the memory descriptor (`mm_struct`) do not drop to zero. In this
specific report, the counters indicate an imbalance of `1` for `MM_FILEPAGES`
and `32` for `MM_SWAPENTS` (which corresponds to exactly 128KB or 32 pages of
swap entries, likely related to a multi-size THP or a batched operation).
Historically, these bugs are almost exclusively pure accounting errors caused by
race conditions or mishandled corner cases in the memory management subsystem
(e.g., `userfaultfd` moving pages between VMAs, `uprobe` replacing pages without
properly updating counters, or `madvise` operations on huge pages).
Because the warning is emitted via `pr_alert()` and does not halt the kernel (no
`panic()` or `BUG()`), it does not directly cause a Denial of Service (DoS) in
production environments. Furthermore, since it is an accounting imbalance rather
than a memory corruption (like a use-after-free or out-of-bounds access), it is
not exploitable for privilege escalation or arbitrary code execution.
**Trigger Context:**
- **Unprivileged / User Namespace:** The bug is triggered by `syzbot` running
unprivileged fuzzing programs (frequently utilizing `userfaultfd`, `madvise`, or
`uprobe` which are accessible to unprivileged users, often within user
namespaces).
- **Remote / Network / Peripheral / Filesystem:** This is a local memory
management issue and cannot be triggered via network packets, malicious
filesystems, or peripheral devices.
- **VM Context:** It is a standard host kernel MM bug and is not related to VM
guest/host transitions or confidential computing contexts.
**Conclusion:**
The bug is a local, unprivileged memory accounting anomaly. It does not pose a
risk of memory corruption, privilege escalation, or system crashes in production
builds.]
|