The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: Hyunwoo Kim <imv4bel@gmail.com>
To: "Lorenzo Stoakes (ARM)" <ljs@kernel.org>
Cc: akpm@linux-foundation.org, david@kernel.org, liam@infradead.org,
	vbabka@kernel.org, rppt@kernel.org, surenb@google.com,
	mhocko@suse.com, linux-mm@kvack.org,
	linux-kernel@vger.kernel.org, imv4bel@gmail.com
Subject: Re: [PATCH] mm/pagewalk: fix stale walk->action escaping walk_pmd_range()
Date: Tue, 11 Aug 2026 00:50:17 +0900	[thread overview]
Message-ID: <annzORC0NgSIsX39@v4bel> (raw)
In-Reply-To: <anmrTaWp2etxX7Fd@lucifer>

On Mon, Aug 10, 2026 at 12:19:54PM +0100, Lorenzo Stoakes (ARM) wrote:
> Since it's 2026 + this is your first patch in mm AFAICT, 

Yeah, my home town is netdev :)

> 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 :)
> 
> On Mon, Aug 10, 2026 at 06:45:17PM +0900, Hyunwoo Kim wrote:
> > 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
> 
> 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.
> 
> > ->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
> 
> Not passing what reset? This is so unclear.
> 
> > 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.
> 
> 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? 

Yes, or more precisely when nothing after it sets walk->action = ACTION_SUBTREE.

> 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 :)

> 
> 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.
> 
> 
> >
> > 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).
> 
> 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.

> 
> >
> > 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.
> 
> 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,

Assisted-by: Claude:claude-opus-5

> 
> >
> > Fixes: 3b89863c3fa4 ("mm/pagewalk: fix race between concurrent split and refault")
> > Cc: stable@vger.kernel.org
> 
> This all seems right.
> 
> > Signed-off-by: Hyunwoo Kim <imv4bel@gmail.com>
> 
> 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

  reply	other threads:[~2026-08-10 15:50 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-10  9:45 [PATCH] mm/pagewalk: fix stale walk->action escaping walk_pmd_range() Hyunwoo Kim
2026-08-10 11:19 ` Lorenzo Stoakes (ARM)
2026-08-10 15:50   ` Hyunwoo Kim [this message]
2026-08-10 15:55     ` Lorenzo Stoakes (ARM)
2026-08-10 18:16       ` Andrew Morton

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=annzORC0NgSIsX39@v4bel \
    --to=imv4bel@gmail.com \
    --cc=akpm@linux-foundation.org \
    --cc=david@kernel.org \
    --cc=liam@infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=ljs@kernel.org \
    --cc=mhocko@suse.com \
    --cc=rppt@kernel.org \
    --cc=surenb@google.com \
    --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