mm/pagewalk: fix stale walk->action escaping walk_pmd_range()
Subsystems (2)
PATCH 1/1 mm/pagewalk: fix stale walk->action escaping walk_pmd_range()
walk_pmd_range() resets walk->action in only one place in its loop body, and that place is after the pmd_none() branch. For a walker with no ->install_pte, that branch continues to the next entry without passing the reset. So if ->pmd_entry() sets ACTION_AGAIN and returns 0, and the PMD has become none by the time the loop restarts at the again label, the reset is skipped. If the remaining entries are all none too, the loop returns 0 with ACTION_AGAIN still set. The ACTION_AGAIN that walk_pte_range() sets when pte_offset_map_lock() fails escapes the same way.
Nothing looked at that value after walk_pmd_range() returned until commit 3b89863c3fa4 ("mm/pagewalk: fix race between concurrent split and refault") turned that into a problem. It added both the PUD check that sets ACTION_AGAIN before the loop is entered and the test in walk_pud_range() that picks the value up right after walk_pmd_range() returns and walks [addr, pud_addr_end(addr, end)) again. That is fine for the PUD check, since none of walk_pmd_range()'s own callbacks have run at that point, but a value that escaped as described above arrives after those callbacks have already covered the range.
For mincore(2) this becomes an out-of-bounds write. ->pmd_entry() and ->pte_hole() advance the walk->private cursor by one byte per page, the buffer is a single page from __get_free_page(), and mincore(2) asks for at most PAGE_SIZE entries at a time, so there is no room to spare. Walking the range a second time pushes the cursor past the end of the buffer, and it does so again every time the race is hit. Reproducing this needs no
privileges: run mincore(2) over a 16 MiB anonymous mapping marked MADV_NOHUGEPAGE while another thread repeatedly faults in a PMD-aligned 2 MiB range inside it and then drops it with madvise(MADV_DONTNEED).
Move the reset to the first statement of the loop body. walk_pud_range() has the same shape and gets the same change; walk_p4d_range() never looks at walk->action, so that hunk keeps the two functions in sync rather than fixing a second bug.
--- mm/pagewalk.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-)
mm/pagewalk.c +2 -4
@@ -126,6 +126,7 @@ static int walk_pmd_range(pud_t *pud, unsigned long addr, unsigned long end, pmd =pmd_offset (pud, addr); do { again: + walk->action = ACTION_SUBTREE; next = pmd_addr_end(addr, end); if (pmd_none(*pmd)) { if (has_install)
@@ -138,8 +139,6 @@ static int walk_pmd_range(pud_t *pud, unsigned long addr, unsigned long end,continue ; } - walk->action =ACTION_SUBTREE ; -/* * This implies that each ->pmd_entry() handler * needs to know about pmd_trans_huge() pmds
@@ -196,6 +195,7 @@ static int walk_pud_range(p4d_t *p4d, unsigned long addr, unsigned long end, pud =pud_offset (p4d, addr); do { again: + walk->action = ACTION_SUBTREE; next = pud_addr_end(addr, end); if (pud_none(*pud)) { if (has_install)
@@ -208,8 +208,6 @@ static int walk_pud_range(p4d_t *p4d, unsigned long addr, unsigned long end,continue ; } - walk->action =ACTION_SUBTREE ; -if (ops->pud_entry) err = ops->pud_entry (pud, addr, next, walk);if (err)
Signature · 1 line
2.43.0
Lorenzo Stoakes (ARM)
reviewerSince it's 2026 + this is your first patch in mm AFAICT, and it's also a very fiddly and specific issue, I do have to ask - was there was any AI involved in making this?
If so you should add an Assisted-by: tag as per
https://docs.kernel.org/process/coding-assistants.html please :)
Quoted 2 lines · Hyunwoo Kim
walk_pmd_range()resetswalk->actionin only one place in its loop body, and that place is after thepmd_none()branch. For a walker with no
Newline after full stop please, this is too many words in one big block.
What is a 'reset'? This doesn't really mean anything. You mean sets walk->action = ACTION_SUBTREE, which is the default action.
Your commit message doesn't mention ACTION_SUBTREE anywhere, it should.
Also what you're saying here is just untrue if you consider reset to be assignment to walk->action (which is a reasonable interpretation) - walk_pmd_range() changes walk->action in _two_ places.
Just be clear that you by reset you mean assigning walk->action = ACTION_SUBTREE.
Quoted 2 lines
->install_pte, that branch continues to the next entry without passing the reset. So if ->pmd_entry() sets
ACTION_AGAINand returns 0, and the PMD has
Not passing what reset? This is so unclear.
Quoted 4 lines
become none by the time the loop restarts at the again label, the reset is skipped. If the remaining entries are all none too, the loop returns 0 with
ACTION_AGAINstill set. TheACTION_AGAINthatwalk_pte_range()sets whenpte_offset_map_lock()fails escapes the same way.
OK so what you mean to say is:
if (ops->pmd_entry) err = ops->pmd_entry(pmd, addr, next, walk); <- 1. sets walk->action = ACTION_AGAIN if (err) break; if (walk->action == ACTION_AGAIN) goto again;
Then above that code:
again:
next = pmd_addr_end(addr, end);
if (pmd_none(*pmd)) { <- 2. This triggers because PMD became empty
if (has_install)
err = __pte_alloc(walk->mm, pmd);
else if (ops->pte_hole)
err = ops->pte_hole(addr, next, depth, walk);
if (err)
break;
if (!has_install)
continue; <- 3. Loop around to the next entry, with
walk->action erroneously set to
ACTION_AGAIN still.
}
walk->action = ACTION_SUBTREE; <- 4. This would reset it EXCEPT if
you are at the end of the range.Then in walk_pud_range():
err = walk_pmd_range(pud, addr, next, walk); if (err) break; if (walk->action == ACTION_AGAIN) <- 5. Uh oh doing a retry for no reason. goto again;
So it's only if you're at the end of the PMD range that it's a problem right? You should say that.
The user-visible problem here is that you can end up calling the callbacks too many times which explicitly breaks mincore.
Given I had to decode this it does make me wonder if you fully understand this or if it's just unclear language. This sort of word salad is very LLM-ish :)
Anyway overall maybe rewrite the commit message to something like:
If ->pmd_entry() sets walk->action = ACTION_AGAIN, the pmd_none() check is retried. The PMD entry may be cleared at the point of retry.
In this case, if walk->ops->install_pte is not specified, the code continues to the next PMD entry in the range without resetting walk->action to ACTION_SUBTREE.
This leaves walk->action erroneously set to ACTION_AGAIN, which is incorrect.
This was incorrect but not problematic up until commit 3b89863c3fa4 ("mm/pagewalk: fix race between concurrent split and refault") which updated walk_pud_range() to check for walk->action == ACTION_AGAIN upon walk_pmd_range()'s return, causing the PUD walk to be retried.
In this case this results in duplicate walk callbacks being invoked, which is erroneous and will break any caller that is not idempotent with respect to this (and waste time for those which are).
A specific example of this breaking things is mincore which walks an internal cursor data structure a byte at a time on assumption that page table entry callbacks are called only once for each entry.
Fix the problem by resetting walk->action to ACTION_SUBTREE prior to the none check.
The pattern also exists in walk_pud_entry() so fix it there too.
Quoted 11 lines
Nothing looked at that value after
walk_pmd_range()returned until commit3b89863c3fa4("mm/pagewalk: fix race between concurrent split and refault") turned that into a problem. It added both the PUD check that setsACTION_AGAINbefore the loop is entered and the test inwalk_pud_range()that picks the value up right afterwalk_pmd_range()returns and walks [addr,pud_addr_end(addr, end)) again. That is fine for the PUD check, since none of walk_pmd_range()'s own callbacks have run at that point, but a value that escaped as described above arrives after those callbacks have already covered the range.
Quoted 9 lines
For mincore(2) this becomes an out-of-bounds write. ->pmd_entry() and ->pte_hole() advance the
walk->privatecursor by one byte per page, the buffer is a single page from__get_free_page(), and mincore(2) asks for at mostPAGE_SIZEentries at a time, so there is no room to spare. Walking the range a second time pushes the cursor past the end of the buffer, and it does so again every time the race is hit. Reproducing this needs no
privileges: run mincore(2) over a 16 MiB anonymous mapping markedMADV_NOHUGEPAGEwhile another thread repeatedly faults in a PMD-aligned 2 MiB range inside it and then drops it withmadvise(MADV_DONTNEED).
Obviouisly see above on commit message.
Is it possible to add a self test that does something like this or is it too racey to be practical?
Quoted 5 lines
Move the reset to the first statement of the loop body.
walk_pud_range()has the same shape and gets the same change;walk_p4d_range()never looks atwalk->action, so that hunk keeps the two functions in sync rather than fixing a second bug.
Your commit message doesn't mention how you discovered this. If it was AI suggesting it (whether locally or sashiko in reply to some review) you should reference it.
If it was simply hardcore code inspection then you should say so too :)
Quoted 3 lines
Fixes 3b89863c3fa4 ("mm/pagewalk: fix race between concurrent split and refault")
This all seems right.
Quoted 1 line
Signed-off-by Hyunwoo Kim <[email protected]>
The fix itself looks right, but you need to address the other feedback and respin (also please send a respin with at least 1 day's delay).
Quoted 43 lines
--- mm/pagewalk.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-)mm/pagewalk.c +2
-5@@ -126,6 +126,7 @@ static int walk_pmd_range(pud_t *pud, unsigned long addr, unsigned long end, pmd =pmd_offset (pud, addr); do { again: + walk->action = ACTION_SUBTREE; next = pmd_addr_end(addr, end); if (pmd_none(*pmd)) { if (has_install)@@ -138,8 +139,6 @@ static int walk_pmd_range(pud_t *pud, unsigned long addr, unsigned long end,continue ; } - walk->action =ACTION_SUBTREE ; -/* * This implies that each ->pmd_entry() handler * needs to know about pmd_trans_huge() pmds @@ -196,6 +195,7 @@ static int walk_pud_range(p4d_t *p4d, unsigned long addr, unsigned long end, pud =pud_offset (p4d, addr); do { again: + walk->action = ACTION_SUBTREE; next = pud_addr_end(addr, end); if (pud_none(*pud)) { if (has_install)@@ -208,8 +208,6 @@ static int walk_pud_range(p4d_t *p4d, unsigned long addr, unsigned long end,continue ; } - walk->action =ACTION_SUBTREE ; -if (ops->pud_entry) err = ops->pud_entry (pud, addr, next, walk);if (err) --
Quoted 2 lines
2.43.0
--
Cheers, Lorenzo
Quoted 1 line · Lorenzo Stoakes (ARM)
Since it's 2026 + this is your first patch in mm AFAICT,
Yeah, my home town is netdev :)
Quoted 76 lines
and it's also a
very fiddly and specific issue, I do have to ask - was there was any AI involved in making this?If so you should add an Assisted-by: tag as per
https://docs.kernel.org/process/coding-assistants.html please :)Quoted 2 lines · Hyunwoo Kim
walk_pmd_range()resetswalk->actionin only one place in its loop body, and that place is after thepmd_none()branch. For a walker with noNewline after full stop please, this is too many words in one big block.
What is a 'reset'? This doesn't really mean anything. You mean sets
walk->action=ACTION_SUBTREE, which is the default action.Your commit message doesn't mention
ACTION_SUBTREEanywhere, it should.Also what you're saying here is just untrue if you consider reset to be assignment to
walk->action(which is a reasonable interpretation) -walk_pmd_range()changeswalk->actionin _two_ places.Just be clear that you by reset you mean assigning
walk->action=ACTION_SUBTREE.Quoted 2 lines
->install_pte, that branch continues to the next entry without passing the reset. So if ->pmd_entry() sets
ACTION_AGAINand returns 0, and the PMD hasNot passing what reset? This is so unclear.
Quoted 4 lines
become none by the time the loop restarts at the again label, the reset is skipped. If the remaining entries are all none too, the loop returns 0 with
ACTION_AGAINstill set. TheACTION_AGAINthatwalk_pte_range()sets whenpte_offset_map_lock()fails escapes the same way.OK so what you mean to say is:
if (ops->pmd_entry) err = ops->pmd_entry(pmd, addr, next, walk); <- 1. sets walk->action = ACTION_AGAIN if (err) break; if (walk->action == ACTION_AGAIN) goto again;Then above that code:
again:
next = pmd_addr_end(addr, end); if (pmd_none(*pmd)) { <- 2. This triggers because PMD became empty if (has_install) err = __pte_alloc(walk->mm, pmd); else if (ops->pte_hole) err = ops->pte_hole(addr, next, depth, walk); if (err) break; if (!has_install) continue; <- 3. Loop around to the next entry, with walk->action erroneously set to ACTION_AGAIN still. } walk->action = ACTION_SUBTREE; <- 4. This would reset it EXCEPT if you are at the end of the range.Then in
walk_pud_range():err = walk_pmd_range(pud, addr, next, walk); if (err) break; if (walk->action == ACTION_AGAIN) <- 5. Uh oh doing a retry for no reason. goto again;
Quoted 2 lines
So it's only if you're at the end of the PMD range that it's a problem right?
Yes, or more precisely when nothing after it sets walk->action = ACTION_SUBTREE.
Quoted 8 lines
You should say that.
The user-visible problem here is that you can end up calling the callbacks too many times which explicitly breaks mincore.
Given I had to decode this it does make me wonder if you fully understand this or if it's just unclear language. This sort of word salad is very LLM-ish :)
The commit message was written with help from the lovely Claude. Not the most readable, admittedly :)
Quoted 56 lines
Anyway overall maybe rewrite the commit message to something like:
If ->pmd_entry() sets
walk->action=ACTION_AGAIN, thepmd_none()check is retried. The PMD entry may be cleared at the point of retry.In this case, if
walk->ops->install_pteis not specified, the code continues to the next PMD entry in the range without resettingwalk->actiontoACTION_SUBTREE.This leaves
walk->actionerroneously set toACTION_AGAIN, which is incorrect.This was incorrect but not problematic up until commit
3b89863c3fa4("mm/pagewalk: fix race between concurrent split and refault") which updatedwalk_pud_range()to check forwalk->action==ACTION_AGAINupon walk_pmd_range()'s return, causing the PUD walk to be retried.In this case this results in duplicate walk callbacks being invoked, which is erroneous and will break any caller that is not idempotent with respect to this (and waste time for those which are).
A specific example of this breaking things is mincore which walks an internal cursor data structure a byte at a time on assumption that page table entry callbacks are called only once for each entry.
Fix the problem by resetting
walk->actiontoACTION_SUBTREEprior to the none check.The pattern also exists in
walk_pud_entry()so fix it there too.Quoted 11 lines
Nothing looked at that value after
walk_pmd_range()returned until commit3b89863c3fa4("mm/pagewalk: fix race between concurrent split and refault") turned that into a problem. It added both the PUD check that setsACTION_AGAINbefore the loop is entered and the test inwalk_pud_range()that picks the value up right afterwalk_pmd_range()returns and walks [addr,pud_addr_end(addr, end)) again. That is fine for the PUD check, since none of walk_pmd_range()'s own callbacks have run at that point, but a value that escaped as described above arrives after those callbacks have already covered the range.Quoted 9 lines
For mincore(2) this becomes an out-of-bounds write. ->pmd_entry() and ->pte_hole() advance the
walk->privatecursor by one byte per page, the buffer is a single page from__get_free_page(), and mincore(2) asks for at mostPAGE_SIZEentries at a time, so there is no room to spare. Walking the range a second time pushes the cursor past the end of the buffer, and it does so again every time the race is hit. Reproducing this needs no
privileges: run mincore(2) over a 16 MiB anonymous mapping markedMADV_NOHUGEPAGEwhile another thread repeatedly faults in a PMD-aligned 2 MiB range inside it and then drops it withmadvise(MADV_DONTNEED).
Quoted 4 lines
Obviouisly see above on commit message.
Is it possible to add a self test that does something like this or is it too racey to be practical?
It looks quite race dependent. Triggering it deterministically would need some of the race window widening tricks from exploit work, which I don't think belongs in a selftest. I can still write one if you'd like.
Quoted 7 lines
Quoted 6 lines
Move the reset to the first statement of the loop body.
walk_pud_range()has the same shape and gets the same change;walk_p4d_range()never looks atwalk->action, so that hunk keeps the two functions in sync rather than fixing a second bug.
Quoted 5 lines
Your commit message doesn't mention how you discovered this. If it was AI suggesting it (whether locally or sashiko in reply to some review) you should reference it.
If it was simply hardcore code inspection then you should say so too :)
It was found by accident while fuzzing a different subsystem. Either way, the fuzzer itself was written by AI, so,
Quoted 11 lines
Quoted 4 lines
Fixes 3b89863c3fa4 ("mm/pagewalk: fix race between concurrent split and refault")This all seems right.
Quoted 1 line
Signed-off-by Hyunwoo Kim <[email protected]>The fix itself looks right, but you need to address the other feedback and respin (also please send a respin with at least 1 day's delay).
OK, will do.
Best regards,
Hyunwoo Kim
Lorenzo Stoakes (ARM)
reviewerQuoted 4 lines · Hyunwoo Kim
Quoted 1 line · Lorenzo Stoakes (ARM)
Since it's 2026 + this is your first patch in mm AFAICT,
Yeah, my home town is netdev :)
Ah I see :) well welcome, this is regardless a useful bug fix so it's appreciated!
Quoted 4 lines
Quoted 2 lines
So it's only if you're at the end of the PMD range that it's a problem right?
Yes, or more precisely when nothing after it sets
walk->action=ACTION_SUBTREE.
Yeah I word this a bit better in my proposed commit message :)
Quoted 6 lines
Quoted 6 lines
You should say that.
The user-visible problem here is that you can end up calling the callbacks too many times which explicitly breaks mincore.
Quoted 6 lines
Quoted 3 lines
Given I had to decode this it does make me wonder if you fully understand this or if it's just unclear language. This sort of word salad is very LLM-ish :)
The commit message was written with help from the lovely Claude. Not the most readable, admittedly :)
Yeah I did wonder :)
I'm totally fine with using an LLM to help with language, especially if English is not your primary language, but obviously it can go... wrong :)
So will always feedback if the commit message is unclear.
LLMs seem to have a habit of constructing word salads that end up effectively like 'the code in English' which ends up confusing more than helping.
Key thing is keeping things as short and clear as possible.
Quoted 6 lines
Quoted 2 lines
Is it possible to add a self test that does something like this or is it too racey to be practical?
It looks quite race dependent. Triggering it deterministically would need some of the race window widening tricks from exploit work, which I don't think belongs in a selftest. I can still write one if you'd like.
Yeah that's what I wondered about. It might still be useful even if it's racey, as long as obviously the _test itself_ isn't flakey :) and also - it can't take too long to run, either.
It'd be good to have a regression test for this.
Quoted 12 lines
Quoted 12 lines
Quoted 7 lines
Move the reset to the first statement of the loop body.
walk_pud_range()has the same shape and gets the same change;walk_p4d_range()never looks atwalk->action, so that hunk keeps the two functions in sync rather than fixing a second bug.Your commit message doesn't mention how you discovered this. If it was AI suggesting it (whether locally or sashiko in reply to some review) you should reference it.
Quoted 6 lines
Quoted 1 line
If it was simply hardcore code inspection then you should say so too :)
It was found by accident while fuzzing a different subsystem. Either way, the fuzzer itself was written by AI, so,
Assisted-by Claude:claude-opus-5
Ack thanks :)
Quoted 6 lines
Quoted 6 lines
Quoted 5 lines
Fixes 3b89863c3fa4 ("mm/pagewalk: fix race between concurrent split and refault")
Quoted 8 lines
Quoted 6 lines
This all seems right.
Quoted 1 line
Signed-off-by Hyunwoo Kim <[email protected]>The fix itself looks right, but you need to address the other feedback and respin (also please send a respin with at least 1 day's delay).
OK, will do.
Great thanks!
Quoted 4 lines
Best regards,
Hyunwoo Kim
--
Cheers, Lorenzo
Andrew Morton
maintainerQuoted 7 lines · On Mon, 10 Aug 2026 16:55:25 +0100 "Lorenzo Stoakes (ARM)" <[email protected]>…
Quoted 2 lines
The commit message was written with help from the lovely Claude. Not the most readable, admittedly :)
Yeah I did wonder :)
I'm totally fine with using an LLM to help with language, especially if English is not your primary language, but obviously it can go... wrong :)
yup. Changelogs from Asian developers have become a lot easier to read in recent times. But please always carefully review the resulting output.
Quoted 4 lines
So will always feedback if the commit message is unclear.
LLMs seem to have a habit of constructing word salads that end up effectively like 'the code in English' which ends up confusing more than helping.
I wonder if it would help to put the initial changelog text through the LLM, but don't show it any code.
Quoted 1 line
Key thing is keeping things as short and clear as possible.
I pasted the original changelog into Gemini asking "make this shorter" and got
Issue
walk_pmd_range() only resets walk->action after the pmd_none() check. For a walker without ->install_pte, a pmd_none() branch jumps to the next entry before resetting walk->action. If ACTION_AGAIN is set and the entry becomes none, the loop can return 0 while leaving ACTION_AGAIN active.
Since commit 3b89863c3fa4, walk_pud_range() reads this leaked ACTION_AGAIN status and rewalks the PMD range. Because callbacks like ->pmd_entry() and ->pte_hole() have already processed the range once, re-executing them advances walk->private beyond the allocated buffer, causing an unprivileged out-of-bounds write (e.g., via mincore(2)).
Fix
Move the walk->action = ACTION_SUBTREE reset to the top of the loop body in walk_pmd_range(). Apply the same update to walk_pud_range() and walk_p4d_range() to maintain code consistency.
Which is at least shorter ;)
We have a description of the userspace-visible runtime effects of the bug, which is good, especially with cc:stable fixes. If there's a bug report or a backtrace to Link: to then please do that.