From: Zi Yan <ziy@nvidia.com>
To: "Lorenzo Stoakes (ARM)" <ljs@kernel.org>
Cc: Andrew Morton <akpm@linux-foundation.org>,
David Hildenbrand <david@kernel.org>,
Rik van Riel <riel@surriel.com>,
"Liam R. Howlett" <liam@infradead.org>,
Vlastimil Babka <vbabka@kernel.org>, Harry Yoo <harry@kernel.org>,
Jann Horn <jannh@google.com>, Lance Yang <lance.yang@linux.dev>,
Kyle Zeng <kylebot@openai.com>,
stable@vger.kernel.org, linux-mm@kvack.org,
linux-kernel@vger.kernel.org
Subject: Re: [PATCH] mm/rmap: don't discard lazyfree THPs at PMD level
Date: Mon, 12 Oct 2026 16:37:34 -0400 [thread overview]
Message-ID: <9CC1C4F0-1C73-4AA4-8D1C-28DB024E2221@nvidia.com> (raw)
In-Reply-To: <asz2domYFnTe2owf@gremlin>
On 12 Oct 2026, at 11:22, Lorenzo Stoakes (ARM) wrote:
> On Mon, Oct 12, 2026 at 10:33:34AM -0400, Zi Yan wrote:
>>> Looking at the code:
>>>
>>> <anon_vma lock>
>>> [reclaim]
>>> < PMD lock >
>>> -> try_to_unmap_one()
>>> -> unmap_huge_pmd_locked()
>>> -> __discard_anon_folio_pmd_locked()
>>> [ clears PMD entry ]
>>
>> The PMD becomes pmd_none().
>
> Yup.
>
>>
>>> ...
>>
>> There are two readers making decisions based on “pmd_none()” locklessly:
>>
>> 1. munmap case is via zap_pmd_range() in mm/memory.c: it skips pmd_none(),
>> so the above folio is not zapped;
>
> Yup I get at that below.
>
> It also doesn't serialise on the PMD lock as a result.
>
> That's really key isn't it?
>
> if (pmd_is_huge(*pmd)) { <- returns false
> if (next - addr != HPAGE_PMD_SIZE)
> __split_huge_pmd(vma, pmd, addr, false);
> else if (zap_huge_pmd(tlb, vma, pmd, addr)) { <- doesn't lock the PMD
> addr = next;
> continue;
> }
>
> So it skips it and nothing happens to the PMD nor is there any serialisation
> between unmap_huge_pmd_locked() and this code.
Right.
>
> try_to_unmap_one() will be called under the PMD lock because of this In
> page_vma_mapped_walk(), I believe:
>
> bool page_vma_mapped_walk(struct page_vma_mapped_walk *pvmw)
> {
> ...
> if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE) &&
> (pmd_trans_huge(pmde) || pmd_is_migration_entry(pmde) ||
> pmd_is_device_private_entry(pmde))) {
> pvmw->ptl = pmd_lock(mm, pvmw->pmd);
> ...
> if (likely(pmd_trans_huge(pmde))) {
> ...
> return true;
> }
> ...
> }
> ...
> }
Yes.
My wording was not precise.
zap_pmd_range() doesn’t take the PTL since it sees pmd_none(), while
try_to_unmap_one() takes the PTL.
>
>>
>> 2. MREMAP_DONTUNMAP case is via move_page_tables() in mm/mremap.c calls
>> get_old_pmd(): get_old_pmd() returns NULL for pmd_none(),
>> so move_page_tables() skips the above folio.
>
> move_page_tables() is called for _all_ mremap() operations. So the same issue
> would occur across mremap().
>
> That bit isn't special to MREMAP_DONTUNMAP.
Right.
>
>>
>>> [ reinstates PMD entry later ]
>>
>>
>> 1. for munmap, unmap_region() calls free_pgtables(). In free_pmd_range(),
>> the restored PMD is treated as pmd_bad() and cleared. And the folio’s
>> rmap is not removed.
>
> Yup I got to that point below :)
>
>>
>> 2. for meremap, dontunmap_complete() unlinks anon_vma but the source VMA
>> still maps the folio.
>
> Well, only if the set_pmd_at() managed to reinstall it AND the PUD page table
> that mapped it still exists.
>
> That set pmd could also UAF on the PMD page table in theory couldn't it?
No, in __discard_anon_folio_pmd_locked(), the caller holds anon_vma read lock;
when mremap() or munmap() calls free_pgtables() to get rid of page tables,
they first call unlink_anon_vmas() and get blocked by the aforementioned read
lock. set_pmd_at() is called while free_pgtables() is blocked, so no UAF here.
>
> It's all really horrible.
And kills my brain.
>
>>
>>>
>>> Concurrent munmap():
>>>
>>> <mmap write lock, downgraded to mmap read lock later>
>>> do_vmi_align_munmap()
>>> -> vms_complete_munmap_vmas()
>>> -> vms_clear_ptes()
>>> -> unmap_region()
>>> -> unmap_vmas() < zaps > <----- EDIT: the real issue, see below
>>> -> free_pgtables()
>>> -> unlink_anon_vmas()
>>> -> < anon_vma lock taken! >
>>> ... clears down page tables (no rmap lock) ...
>>>
>>> Is the issue actually the zapping in unmap_vmas(), which doesn't seem to be
>>> protected by rmap lock at all, meaning that _that_ can happen with
>>> concurrent munmap()?
>>
>> No. zap and rmap walkers are serialized by PTL. The reason is described above.
>>
>
> Well, no, that's not quite right AFAICT - as above the PMD PTL _is not taken_
> because the PMD entry is cleared.
>
> So the serialisation that would otherwise exist between the try_to_unmap_one()
> and zap_pmd_range() doesn't happen.
>
>>>
>>> So unmap_vmas() doesn't correctly populate the mmu_gather struct for the
>>> range, meaning that the underlying page isn't freed and the refcount,
>>> mapcount etc. are all left as they were?
>>
>> Yes to the underlying page isn’t freed.
>
> The refcount and mapcount etc. would be correct? I think the refcount would be
> such that it permanently leaks and the mapcount would be incorrect or only
> accidentally correct?
refcount and mapcount would be wrong in the munmap case. Both are correct
in MREMAP_DONTUNMAP case, since the folio is still mapped in the source VMA
but its anon_vma says it belongs to dst VMA. When dst VMA is unmapped,
the anon_vma is freed, but the folio is still mapped and its ->mapping
points to a freed anon_vma. A rmap code checking folio->mapping is a UAF.
>
>>
>>>
>>> MREMAP_DONTUNMAP seems a bit of a bizarre callout, but I suppose it is the
>>> other case in which (under certain, likely, circumstances
>>> e.g. non-partial/self-merging remap) an unlink_anon_vmas()'s call is done.
>>>
>>> With normal mremap() that'd happen via an unmap of the original VMA.
>>>
>>>> rmap walk uses the freed anon_vma.
>>>>
>>>> Using an invalidated PMD instead of a cleared one requires additional arch
>>>> code fixes. Instead, disable the PMD level discard of lazyfree THPs, as
>>>> before commit 735ecdfaf4e8 ("mm/vmscan: avoid split lazyfree THP during
>>>> shrink_folio_list()").
>>>
>>> Again it feels like this commit message assumes a bunch of information that
>>> isn't presented here.
>>
>> 1. SPARC has thp_pte_count-- in its pmdp_invalidate(), but set_pmd_at()
>> does not add it back, since the code assumes pmdp_invalidate() is only
>> used in THP splitting code.
>>
>> 2. GUP-fast accepts an invalidate PMD on s390, risk, loongarch. They
>> need to fix it to prevent GUP-fast from succeeding for this new
>> pmdp_invalidate().
>
> Thanks that's useful to know! And ugh.
>
> They seem like they are existing bugs effectively in pmdp_invalidate()? Or stuff
> that we would want to fix anyway I guess?
SPARC’s thp_pte_count might cause other issues, since its code only considers
huge pmd splitting. powerpc’s page table check might have an issue.
>
> In general the point of all the above discussion is to encourage a commit
> message that conveys this information clearly.
>
> When I read the patch first there was a lot of 'huh' and that's a sign that the
> commit msg doesn't say enough.
>
>>>> diff --git a/mm/rmap.c b/mm/rmap.c
>>>> index 805db93fe0428..1131b76bbbc28 100644
>>>> --- a/mm/rmap.c
>>>> +++ b/mm/rmap.c
>>>> @@ -2275,17 +2275,6 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
>>>> }
>>>>
>>>> if (!pvmw.pte) {
>>>> - if (folio_test_lazyfree(folio)) {
>>>> - if (unmap_huge_pmd_locked(vma, pvmw.address, pvmw.pmd, folio))
>>>
>>> This is the only caller of unmap_huge_pmd_locked(), making that and
>>> __discard_anon_folio_pmd_locked() dead code, shouldn't we remove all of it?
>>>
>>> Otherwise we're leaving dead code around.
>>
>> 1) it keeps the backport small, 2) I am going to restore the function and
>> this reduce the code churn.
>>
>> I can remove the dead code if you think that is better.
>
> No, if you plan to reinstate it (soon!) then that's fine, but please
> explain that you're intentionally doing that in the commit message please!
Sure. I will try to come up with a concise but comprehensive message to cover
both the issue and why pmdp_invalidate() does not work itself.
Best Regards,
Yan, Zi
next prev parent reply other threads:[~2026-10-12 20:37 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-10 2:08 Zi Yan
2026-10-10 2:14 ` Kyle Zeng
2026-10-10 2:28 ` Lance Yang
2026-10-12 10:25 ` David Hildenbrand (Arm)
2026-10-12 12:07 ` Lorenzo Stoakes (ARM)
2026-10-12 14:33 ` Zi Yan
2026-10-12 14:39 ` David Hildenbrand (Arm)
2026-10-12 15:22 ` Lorenzo Stoakes (ARM)
2026-10-12 15:22 ` Lorenzo Stoakes (ARM)
2026-10-12 20:37 ` Zi Yan [this message]
2026-10-13 14:29 ` Lorenzo Stoakes (ARM)
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=9CC1C4F0-1C73-4AA4-8D1C-28DB024E2221@nvidia.com \
--to=ziy@nvidia.com \
--cc=akpm@linux-foundation.org \
--cc=david@kernel.org \
--cc=harry@kernel.org \
--cc=jannh@google.com \
--cc=kylebot@openai.com \
--cc=lance.yang@linux.dev \
--cc=liam@infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=ljs@kernel.org \
--cc=riel@surriel.com \
--cc=stable@vger.kernel.org \
--cc=vbabka@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®