All of lore.kernel.org
 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.