Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: "Lorenzo Stoakes (ARM)" <ljs@kernel.org>
To: "David Hildenbrand (Arm)" <david@kernel.org>
Cc: Hengbin Zhang <uqbarz@gmail.com>,
	 Andrew Morton <akpm@linux-foundation.org>,
	linux-mm@kvack.org, linux-kernel@vger.kernel.org,
	 stable@vger.kernel.org, Zi Yan <ziy@nvidia.com>,
	 Baolin Wang <baolin.wang@linux.alibaba.com>,
	"Liam R. Howlett" <liam@infradead.org>,
	 Nico Pache <npache@redhat.com>,
	Ryan Roberts <ryan.roberts@arm.com>, Dev Jain <dev.jain@arm.com>,
	 Barry Song <baohua@kernel.org>,
	Lance Yang <lance.yang@linux.dev>,
	 Usama Arif <usama.arif@linux.dev>
Subject: Re: [RFC PATCH v3] mm/thp: serialize huge-zero folio state transitions
Date: Mon, 27 Jul 2026 19:09:05 +0100	[thread overview]
Message-ID: <ameC4IWCOVBGlXZu@lucifer> (raw)
In-Reply-To: <b915b058-f5b1-423a-a835-5891e7997080@kernel.org>

TL;DR - after spending far too long on this - this is very subtle and I am
not confident you understand the patch, I wonder if the fix is possibly
AI-generated also?

This is really subtle stuff and requires backports, so I think it's better
if I send a patch myself.

(I think this is less egregious than it might seem in that the lock
suggestion is ultimately David's in any case.)

I will acknowledge this submission in the commit message, however.

It seems the race is:

static unsigned long shrink_huge_zero_folio_scan(struct shrinker *shrink,
						 struct shrink_control *sc)
{
	if (atomic_cmpxchg(&huge_zero_refcount, 1, 0) == 1) {   <-------------------------- 1. sets to 0
		struct folio *zero_folio = xchg(&huge_zero_folio, NULL); < preempted for a very very long time>
		BUG_ON(zero_folio == NULL);
		WRITE_ONCE(huge_zero_pfn, ~0UL);                <------------------------- 6. Overwrites the valid PFN
		folio_put(zero_folio);
		return HPAGE_PMD_NR;
	}

	return 0;
}

static bool get_huge_zero_folio(void)
{
	struct folio *zero_folio;
retry:
	if (likely(atomic_inc_not_zero(&huge_zero_refcount))) <--------------------  2. sees the zero
		return true;

	zero_folio = folio_alloc((GFP_TRANSHUGE | __GFP_ZERO | __GFP_ZEROTAGS) & <--- 3. allocates (magically, very very quickly)
				 ~__GFP_MOVABLE,
			HPAGE_PMD_ORDER);
	if (!zero_folio) {
		count_vm_event(THP_ZERO_PAGE_ALLOC_FAILED);
		return false;
	}
	/* Ensure zero folio won't have large_rmappable flag set. */
	folio_clear_large_rmappable(zero_folio);
	preempt_disable();
	if (cmpxchg(&huge_zero_folio, NULL, zero_folio)) { <---------------- 4. no concurrent setter so branch not taken
		preempt_enable();
		folio_put(zero_folio);
		goto retry;
	}
	WRITE_ONCE(huge_zero_pfn, folio_pfn(zero_folio)); <----------------- 5. Writes the PFN first despite not being able to be reordered

	/* We take additional reference here. It will be put back by shrinker */
	atomic_set(&huge_zero_refcount, 2);
	preempt_enable();
	count_vm_event(THP_ZERO_PAGE_ALLOC);
	return true;
}

?

In general I don't know why we're not just making
CONFIG_PERSISTENT_HUGE_ZERO_FOLIO mandatory for CONFIG_TRANSPARENT_HUGEPAGE.

If you're embedded, don't enabled THP (which will up your reserves a bunch for
khugepaged anyway).

On Mon, Jul 27, 2026 at 06:08:53PM +0200, David Hildenbrand (Arm) wrote:
> On 7/27/26 17:40, Hengbin Zhang wrote:
> > The nonpersistent huge-zero shrinker clears huge_zero_folio and then

NIT: insert a hyphen: non-persistent.

> > invalidates huge_zero_pfn. A concurrent fault can publish a replacement

'Invalidates'? How do you mean?

You mean setting it to NULL? Clearer to say that.

> > folio and PFN between those updates, after which the old shrinker
> > invalidates the replacement generation's PFN identity.

I don't understand what you mean generation or PFN identity?


> >
> > A later partial mprotect() can then misidentify the live special PMD and
> > enter the ordinary anonymous THP split path.

What is a live special PMD?

Please don't say special :) that term is so overloaded in mm.

'...can then fail to identify the huge zero folio which then incorrectly enters
the anonymous THP split path'.

But I'd even not mention mprotect() specifically, if the huge zero folio can be
misidentified that's already a problem.

> >
> > A writer-side spinlock keeps huge_zero_folio and huge_zero_pfn updates
> > from different generations together. Lockless getters still use the

I'm very confused by what you mean.

> > refcount fast path. atomic_set_release() publishes an initialized new
> > generation before a successful atomic_inc_not_zero() can admit a getter
> > on weakly ordered architectures.

Overall I am not confident you understand this so as per above I think

> >
> > Suggested-by: David Hildenbrand <david@kernel.org>

Should be a S-o-b and David should handle this.

> > Link: https://lore.kernel.org/r/20260724100509.2300200-1-uqbarz@gmail.com
>
> We want a Fixes: tag + Cc: stable.

Yup.

>
> Can you dig up the proper Fixes commit? Thanks
>
> > Signed-off-by: Hengbin Zhang <uqbarz@gmail.com>
> > ---
> >  mm/huge_memory.c | 22 +++++++++++++++-------
> >  1 file changed, 15 insertions(+), 7 deletions(-)
> >
> > diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> > index b5d1e9d4463d..79bec4495540 100644
> > --- a/mm/huge_memory.c
> > +++ b/mm/huge_memory.c
> > @@ -78,6 +78,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
> >  static bool split_underused_thp = true;
> >
> >  static atomic_t huge_zero_refcount;
> > +static DEFINE_SPINLOCK(huge_zero_lock);
> >  struct folio *huge_zero_folio __read_mostly;
> >  unsigned long huge_zero_pfn __read_mostly = ~0UL;
> >  unsigned long huge_anon_orders_always __read_mostly;
> > @@ -237,17 +238,18 @@ static bool get_huge_zero_folio(void)
> >  	}
> >  	/* Ensure zero folio won't have large_rmappable flag set. */
> >  	folio_clear_large_rmappable(zero_folio);
> > -	preempt_disable();
> > -	if (cmpxchg(&huge_zero_folio, NULL, zero_folio)) {
> > -		preempt_enable();
> > +	spin_lock(&huge_zero_lock);
> > +	if (READ_ONCE(huge_zero_folio)) {
>
> Same comment as below: who could possibly modify this value concurrently? I
> don't think we need the READ_ONCE.

Yeah I don't think so either.

>
> > +		spin_unlock(&huge_zero_lock);
> >  		folio_put(zero_folio);
> >  		goto retry;
> >  	}
> >  	WRITE_ONCE(huge_zero_pfn, folio_pfn(zero_folio));
> > +	WRITE_ONCE(huge_zero_folio, zero_folio);

This is still necessary however for concurrent lockless readers.

> > +	spin_unlock(&huge_zero_lock);
> >
> > -	/* We take additional reference here. It will be put back by shrinker */
> > -	atomic_set(&huge_zero_refcount, 2);
> > -	preempt_enable();
> > +	/* Publish the identity before admitting lockless getters. */

You're deleting the comment about an additional reference...

And this comment doesn't make any sense really.

> > +	atomic_set_release(&huge_zero_refcount, 2);
>
> Why do we need the _release semantics here when we just did a spin_unlock()?

This is really suspect, as I had AI look at this and it also suggested
exactly what you did here, and I'm not confident you understand it.

Are you generating this code using AI without disclosing it against the
kernel policy on this?

See https://kernel.org/doc/html/latest/process/coding-assistants.html

Can you give a detailed, coherent response as to why you chose this, if it
was not AI-generated? A human-sounding one? :)

Anyway.

AI says this actually _is_ necessary.

And using my human brain to think about it:

static bool get_huge_zero_folio(void)
{
	struct folio *zero_folio;
retry:
	if (likely(atomic_inc_not_zero(&huge_zero_refcount)))
		return true;

	zero_folio = folio_alloc((GFP_TRANSHUGE | __GFP_ZERO | __GFP_ZEROTAGS) &
				 ~__GFP_MOVABLE,
			HPAGE_PMD_ORDER);
	if (!zero_folio) {
		count_vm_event(THP_ZERO_PAGE_ALLOC_FAILED);
		return false;
       	}
	/* Ensure zero folio won't have large_rmappable flag set. */
	folio_clear_large_rmappable(zero_folio);
	spin_lock(&huge_zero_lock);                          <--- acquire semantics
	if (huge_zero_folio) {                                            |			   <|
		spin_unlock(&huge_zero_lock);                             v			   <|
		folio_put(zero_folio);			  All loads/stores AFTER appear AFTER	   <|
		goto retry;									   <|
	}                                                  All loads/stores BEFORE appear BEFORE   <|
	WRITE_ONCE(huge_zero_pfn, folio_pfn(zero_folio));                 ^			   <| Anywhere here.
	WRITE_ONCE(huge_zero_folio, zero_folio);                          |			   <|
	spin_unlock(&huge_zero_lock);                         <--- release semantics               <|
                                                                                                   <|
	/* We take additional reference here. It will be put back by shrinker */                   <|
	atomic_set(&huge_zero_refcount, 2); ---- relaxed ordering so could be moved ----------------|

	count_vm_event(THP_ZERO_PAGE_ALLOC);
	return true;
}

So yeah I think that should be atomic_set_release() in order to prevent
that occurring before the critical section.

The retry stuff is a bit suspect now, how could we be concurrently raced in
order to require a retry? I don't think so.

It's needed because it has to pair with atomic_inc_not_zero() to prevent a
concurrent get_huge_zero_folio() invocation from incorrectly returning
before the critical section is completely finished.

>
> >  	count_vm_event(THP_ZERO_PAGE_ALLOC);
> >  	return true;
> >  }
> > @@ -298,9 +300,15 @@ static unsigned long shrink_huge_zero_folio_scan(struct shrinker *shrink,
> >  						 struct shrink_control *sc)
> >  {
> >  	if (atomic_cmpxchg(&huge_zero_refcount, 1, 0) == 1) {
> > -		struct folio *zero_folio = xchg(&huge_zero_folio, NULL);
> > +		struct folio *zero_folio;
> > +
> > +		spin_lock(&huge_zero_lock);
> > +		zero_folio = READ_ONCE(huge_zero_folio);
>
> Why do we need a READ_ONCE? Nobody should be legally be allowed to change this
> variable concurrently, no?

Yup.

>
> >  		BUG_ON(zero_folio == NULL);
> >  		WRITE_ONCE(huge_zero_pfn, ~0UL);
> > +		WRITE_ONCE(huge_zero_folio, NULL);
> > +		spin_unlock(&huge_zero_lock);
> > +
> >  		folio_put(zero_folio);
> >  		return HPAGE_PMD_NR;
> >  	}
>
>
> --
> Cheers,
>
> David

Thanks, Lorenzo


  reply	other threads:[~2026-07-27 18:09 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-24 10:05 [RFC PATCH] mm/thp: order huge zero folio PFN invalidation before removal Hengbin Zhang
2026-07-24 18:48 ` David Hildenbrand (Arm)
2026-07-27 13:09   ` Hengbin Zhang
2026-07-27 13:18   ` [RFC PATCH v2] mm/thp: serialize huge-zero folio state transitions Hengbin Zhang
2026-07-27 13:40     ` David Hildenbrand (Arm)
2026-07-27 15:40       ` [RFC PATCH v3] " Hengbin Zhang
2026-07-27 15:42         ` Lorenzo Stoakes (ARM)
2026-07-27 15:54           ` Hengbin Zhang
2026-07-27 16:08         ` David Hildenbrand (Arm)
2026-07-27 18:09           ` Lorenzo Stoakes (ARM) [this message]
2026-07-27 18:10             ` 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=ameC4IWCOVBGlXZu@lucifer \
    --to=ljs@kernel.org \
    --cc=akpm@linux-foundation.org \
    --cc=baohua@kernel.org \
    --cc=baolin.wang@linux.alibaba.com \
    --cc=david@kernel.org \
    --cc=dev.jain@arm.com \
    --cc=lance.yang@linux.dev \
    --cc=liam@infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=npache@redhat.com \
    --cc=ryan.roberts@arm.com \
    --cc=stable@vger.kernel.org \
    --cc=uqbarz@gmail.com \
    --cc=usama.arif@linux.dev \
    --cc=ziy@nvidia.com \
    /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