Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [RFC PATCH] mm/thp: order huge zero folio PFN invalidation before removal
@ 2026-07-24 10:05 Hengbin Zhang
  2026-07-24 18:48 ` David Hildenbrand (Arm)
  0 siblings, 1 reply; 11+ messages in thread
From: Hengbin Zhang @ 2026-07-24 10:05 UTC (permalink / raw)
  To: linux-mm, akpm, david, ljs; +Cc: ziy, linux-kernel, Hengbin Zhang

The nonpersistent huge-zero shrinker removes huge_zero_folio with
xchg() and then invalidates huge_zero_pfn. A concurrent fault can
publish a new folio and its PFN between these operations, after which
the old shrinker invalidates the new generation's PFN identity.

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

Invalidate huge_zero_pfn before making huge_zero_folio NULL. A getter
which observes a zero refcount while the old pointer is still present
cannot publish a new folio: its cmpxchg() fails and it retries. A
getter which succeeds does so after the invalidation and publishes the
new PFN afterwards.

Fixes: 3b77e8c8cde5 ("mm/thp: make is_huge_zero_pmd() safe and quicker")

Signed-off-by: Hengbin Zhang <uqbarz@gmail.com>
---
 mm/huge_memory.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index b5d1e9d4463d..fcc492368160 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -298,9 +298,11 @@ 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);
-		BUG_ON(zero_folio == NULL);
+		struct folio *zero_folio;
+
 		WRITE_ONCE(huge_zero_pfn, ~0UL);
+		zero_folio = xchg(&huge_zero_folio, NULL);
+		BUG_ON(zero_folio == NULL);
 		folio_put(zero_folio);
 		return HPAGE_PMD_NR;
 	}
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [RFC PATCH] mm/thp: order huge zero folio PFN invalidation before removal
  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
  0 siblings, 2 replies; 11+ messages in thread
From: David Hildenbrand (Arm) @ 2026-07-24 18:48 UTC (permalink / raw)
  To: Hengbin Zhang, linux-mm, akpm, ljs; +Cc: ziy, linux-kernel

Hi,

How was this issue identified or observed?

On 7/24/26 12:05, Hengbin Zhang wrote:
> The nonpersistent huge-zero shrinker removes huge_zero_folio with
> xchg() and then invalidates huge_zero_pfn. A concurrent fault can
> publish a new folio and its PFN between these operations, after which
> the old shrinker invalidates the new generation's PFN identity.


You mean, the freeing path does

1) xchg(&huge_zero_folio, NULL);
2) WRITE_ONCE(huge_zero_pfn, ~0UL);

Whereby the allocation path does

1) cmpxchg(&huge_zero_folio, NULL, zero_folio)
2) WRITE_ONCE(huge_zero_pfn, folio_pfn(zero_folio));

Sashiko also IMHO correctly complains about a missing barrier when we re-set the
refcount.

So I wonder whether we should just stop these cmpxch games and just use a
spinlock around updating huge_zero_folio+huge_zero_pfn?

-- 
Cheers,

David


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [RFC PATCH] mm/thp: order huge zero folio PFN invalidation before removal
  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
  1 sibling, 0 replies; 11+ messages in thread
From: Hengbin Zhang @ 2026-07-27 13:09 UTC (permalink / raw)
  To: David Hildenbrand (Arm); +Cc: linux-mm, akpm, ljs, ziy, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 1858 bytes --]

Hi David,

The issue was first flagged by an AI-assisted review while I was
analyzing the THP code. I then traced the huge-zero folio lifecycle and
confirmed the interleaving. The window is difficult to hit normally, but
it can be reproduced with test-only instrumentation.

Regarding Sashiko's weak-memory-ordering concern, I think that
atomic_set_release(&huge_zero_refcount, 2) would provide the required
release semantics when publishing a newly initialized huge-zero folio.
However, this only solves the publication ordering between the writer
and lockless readers.

I think your suggestion might be a better choice. A short writer-side
spinlock
around the huge_zero_folio/huge_zero_pfn state transition looks like a
clearer solution.
I will send a spinlock-based version for further revision.

Thanks,
Hengbin

On Sat, Jul 25, 2026 at 2:48 AM David Hildenbrand (Arm) <david@kernel.org>
wrote:

> Hi,
>
> How was this issue identified or observed?
>
> On 7/24/26 12:05, Hengbin Zhang wrote:
> > The nonpersistent huge-zero shrinker removes huge_zero_folio with
> > xchg() and then invalidates huge_zero_pfn. A concurrent fault can
> > publish a new folio and its PFN between these operations, after which
> > the old shrinker invalidates the new generation's PFN identity.
>
>
> You mean, the freeing path does
>
> 1) xchg(&huge_zero_folio, NULL);
> 2) WRITE_ONCE(huge_zero_pfn, ~0UL);
>
> Whereby the allocation path does
>
> 1) cmpxchg(&huge_zero_folio, NULL, zero_folio)
> 2) WRITE_ONCE(huge_zero_pfn, folio_pfn(zero_folio));
>
> Sashiko also IMHO correctly complains about a missing barrier when we
> re-set the
> refcount.
>
> So I wonder whether we should just stop these cmpxch games and just use a
> spinlock around updating huge_zero_folio+huge_zero_pfn?
>
> --
> Cheers,
>
> David
>

[-- Attachment #2: Type: text/html, Size: 2307 bytes --]

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [RFC PATCH v2] mm/thp: serialize huge-zero folio state transitions
  2026-07-24 18:48 ` David Hildenbrand (Arm)
  2026-07-27 13:09   ` Hengbin Zhang
@ 2026-07-27 13:18   ` Hengbin Zhang
  2026-07-27 13:40     ` David Hildenbrand (Arm)
  1 sibling, 1 reply; 11+ messages in thread
From: Hengbin Zhang @ 2026-07-27 13:18 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand
  Cc: linux-mm, linux-kernel, stable, Hengbin Zhang, Lorenzo Stoakes,
	Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
	Dev Jain, Barry Song, Lance Yang, Usama Arif

The nonpersistent huge-zero shrinker clears huge_zero_folio and then
invalidates huge_zero_pfn. A concurrent fault can publish a replacement
folio and PFN between those updates, after which the old shrinker
invalidates the replacement generation's PFN identity.

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

A writer-side spinlock keeps huge_zero_folio and huge_zero_pfn updates
from different generations together. Lockless getters still use the
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.

The race was reproduced using test-only instrumentation that widens the
shrinker/allocation window; that instrumentation is not included here.

Suggested-by: David Hildenbrand <david@kernel.org>
Link: https://lore.kernel.org/r/20260724100509.2300200-1-uqbarz@gmail.com
Signed-off-by: Hengbin Zhang <uqbarz@gmail.com>
---
 mm/huge_memory.c | 36 +++++++++++++++++++++++-------------
 1 file changed, 23 insertions(+), 13 deletions(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index b5d1e9d4463d..89771a9b282d 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,19 @@ 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)) {
+		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);
+
+	/* Publish the identity before admitting lockless getters. */
+	atomic_set_release(&huge_zero_refcount, 2);
+	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();
 	count_vm_event(THP_ZERO_PAGE_ALLOC);
 	return true;
 }
@@ -297,15 +300,22 @@ static unsigned long shrink_huge_zero_folio_count(struct shrinker *shrink,
 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);
-		BUG_ON(zero_folio == NULL);
-		WRITE_ONCE(huge_zero_pfn, ~0UL);
-		folio_put(zero_folio);
-		return HPAGE_PMD_NR;
+	struct folio *zero_folio;
+
+	spin_lock(&huge_zero_lock);
+	if (atomic_cmpxchg(&huge_zero_refcount, 1, 0) != 1) {
+		spin_unlock(&huge_zero_lock);
+		return 0;
 	}
 
-	return 0;
+	zero_folio = READ_ONCE(huge_zero_folio);
+	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;
 }
 
 static struct shrinker *huge_zero_folio_shrinker;
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [RFC PATCH v2] mm/thp: serialize huge-zero folio state transitions
  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
  0 siblings, 1 reply; 11+ messages in thread
From: David Hildenbrand (Arm) @ 2026-07-27 13:40 UTC (permalink / raw)
  To: Hengbin Zhang, Andrew Morton
  Cc: linux-mm, linux-kernel, stable, Lorenzo Stoakes, Zi Yan,
	Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts, Dev Jain,
	Barry Song, Lance Yang, Usama Arif

On 7/27/26 15:18, Hengbin Zhang wrote:
> The nonpersistent huge-zero shrinker clears huge_zero_folio and then
> invalidates huge_zero_pfn. A concurrent fault can publish a replacement
> folio and PFN between those updates, after which the old shrinker
> invalidates the replacement generation's PFN identity.
> 
> A later partial mprotect() can then misidentify the live special PMD and
> enter the ordinary anonymous THP split path.
> 
> A writer-side spinlock keeps huge_zero_folio and huge_zero_pfn updates
> from different generations together. Lockless getters still use the
> 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.
> 
> The race was reproduced using test-only instrumentation that widens the
> shrinker/allocation window; that instrumentation is not included here.
> 
> Suggested-by: David Hildenbrand <david@kernel.org>
> Link: https://lore.kernel.org/r/20260724100509.2300200-1-uqbarz@gmail.com
> Signed-off-by: Hengbin Zhang <uqbarz@gmail.com>
> ---
>  mm/huge_memory.c | 36 +++++++++++++++++++++++-------------
>  1 file changed, 23 insertions(+), 13 deletions(-)
> 
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index b5d1e9d4463d..89771a9b282d 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,19 @@ 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)) {
> +		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);
> +
> +	/* Publish the identity before admitting lockless getters. */
> +	atomic_set_release(&huge_zero_refcount, 2);
> +	spin_unlock(&huge_zero_lock);

Can't we just keep the

atomic_set(&huge_zero_refcount, 2);

after the spin_unlock() ?

>  
> -	/* 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;
>  }
> @@ -297,15 +300,22 @@ static unsigned long shrink_huge_zero_folio_count(struct shrinker *shrink,
>  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);
> -		BUG_ON(zero_folio == NULL);
> -		WRITE_ONCE(huge_zero_pfn, ~0UL);
> -		folio_put(zero_folio);
> -		return HPAGE_PMD_NR;
> +	struct folio *zero_folio;
> +
> +	spin_lock(&huge_zero_lock);
> +	if (atomic_cmpxchg(&huge_zero_refcount, 1, 0) != 1) {
> +		spin_unlock(&huge_zero_lock);
> +		return 0;
>  	}

Do we really have to move that refcount modifications under the lock?

I'd assume it's sufficient to only move setting huge_zero_pfn+huge_zero_folio
under the spinlock.

-- 
Cheers,

David


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [RFC PATCH v3] mm/thp: serialize huge-zero folio state transitions
  2026-07-27 13:40     ` David Hildenbrand (Arm)
@ 2026-07-27 15:40       ` Hengbin Zhang
  2026-07-27 15:42         ` Lorenzo Stoakes (ARM)
  2026-07-27 16:08         ` David Hildenbrand (Arm)
  0 siblings, 2 replies; 11+ messages in thread
From: Hengbin Zhang @ 2026-07-27 15:40 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand
  Cc: linux-mm, linux-kernel, stable, Hengbin Zhang, Lorenzo Stoakes,
	Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
	Dev Jain, Barry Song, Lance Yang, Usama Arif

The nonpersistent huge-zero shrinker clears huge_zero_folio and then
invalidates huge_zero_pfn. A concurrent fault can publish a replacement
folio and PFN between those updates, after which the old shrinker
invalidates the replacement generation's PFN identity.

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

A writer-side spinlock keeps huge_zero_folio and huge_zero_pfn updates
from different generations together. Lockless getters still use the
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.

Suggested-by: David Hildenbrand <david@kernel.org>
Link: https://lore.kernel.org/r/20260724100509.2300200-1-uqbarz@gmail.com
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)) {
+		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);
+	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. */
+	atomic_set_release(&huge_zero_refcount, 2);
 	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);
 		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;
 	}
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [RFC PATCH v3] mm/thp: serialize huge-zero folio state transitions
  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)
  1 sibling, 1 reply; 11+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-07-27 15:42 UTC (permalink / raw)
  To: Hengbin Zhang
  Cc: Andrew Morton, David Hildenbrand, linux-mm, linux-kernel, stable,
	Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
	Dev Jain, Barry Song, Lance Yang, Usama Arif

Please stop sending new revisions in response to review!

Respond to the review, then send the new revision as an entirely separate email.

Also do not respin this quickly, leave at least a delay of a day between respins
so people can have a chance to keep track and review, the workload is high
enough already.

Thanks, Lorenzo


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [RFC PATCH v3] mm/thp: serialize huge-zero folio state transitions
  2026-07-27 15:42         ` Lorenzo Stoakes (ARM)
@ 2026-07-27 15:54           ` Hengbin Zhang
  0 siblings, 0 replies; 11+ messages in thread
From: Hengbin Zhang @ 2026-07-27 15:54 UTC (permalink / raw)
  To: Lorenzo Stoakes (ARM)
  Cc: Andrew Morton, David Hildenbrand, linux-mm, linux-kernel, stable,
	Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
	Dev Jain, Barry Song, Lance Yang, Usama Arif

[-- Attachment #1: Type: text/plain, Size: 850 bytes --]

Hi Lorenzo,

Thanks for the reminder. I apologize for not following the expected
submission workflow.

I'm new to the community and will ensure future revisions are sent as
separate emails after responding to reviews. I will also allow sufficient
time between revisions so reviewers can follow up and provide feedback.

Thanks again for your guidance and patience.

Best regards,
Hengbin

On Mon, Jul 27, 2026 at 11:43 PM Lorenzo Stoakes (ARM) <ljs@kernel.org>
wrote:

> Please stop sending new revisions in response to review!
>
> Respond to the review, then send the new revision as an entirely separate
> email.
>
> Also do not respin this quickly, leave at least a delay of a day between
> respins
> so people can have a chance to keep track and review, the workload is high
> enough already.
>
> Thanks, Lorenzo
>

[-- Attachment #2: Type: text/html, Size: 1287 bytes --]

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [RFC PATCH v3] mm/thp: serialize huge-zero folio state transitions
  2026-07-27 15:40       ` [RFC PATCH v3] " Hengbin Zhang
  2026-07-27 15:42         ` Lorenzo Stoakes (ARM)
@ 2026-07-27 16:08         ` David Hildenbrand (Arm)
  2026-07-27 18:09           ` Lorenzo Stoakes (ARM)
  1 sibling, 1 reply; 11+ messages in thread
From: David Hildenbrand (Arm) @ 2026-07-27 16:08 UTC (permalink / raw)
  To: Hengbin Zhang, Andrew Morton
  Cc: linux-mm, linux-kernel, stable, Lorenzo Stoakes, Zi Yan,
	Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts, Dev Jain,
	Barry Song, Lance Yang, Usama Arif

On 7/27/26 17:40, Hengbin Zhang wrote:
> The nonpersistent huge-zero shrinker clears huge_zero_folio and then
> invalidates huge_zero_pfn. A concurrent fault can publish a replacement
> folio and PFN between those updates, after which the old shrinker
> invalidates the replacement generation's PFN identity.
> 
> A later partial mprotect() can then misidentify the live special PMD and
> enter the ordinary anonymous THP split path.
> 
> A writer-side spinlock keeps huge_zero_folio and huge_zero_pfn updates
> from different generations together. Lockless getters still use the
> 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.
> 
> Suggested-by: David Hildenbrand <david@kernel.org>
> Link: https://lore.kernel.org/r/20260724100509.2300200-1-uqbarz@gmail.com

We want a Fixes: tag + Cc: stable.

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.

> +		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);
> +	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. */
> +	atomic_set_release(&huge_zero_refcount, 2);

Why do we need the _release semantics here when we just did a spin_unlock()?

>  	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?

>  		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


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [RFC PATCH v3] mm/thp: serialize huge-zero folio state transitions
  2026-07-27 16:08         ` David Hildenbrand (Arm)
@ 2026-07-27 18:09           ` Lorenzo Stoakes (ARM)
  2026-07-27 18:10             ` Lorenzo Stoakes (ARM)
  0 siblings, 1 reply; 11+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-07-27 18:09 UTC (permalink / raw)
  To: David Hildenbrand (Arm)
  Cc: Hengbin Zhang, Andrew Morton, linux-mm, linux-kernel, stable,
	Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
	Dev Jain, Barry Song, Lance Yang, Usama Arif

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


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [RFC PATCH v3] mm/thp: serialize huge-zero folio state transitions
  2026-07-27 18:09           ` Lorenzo Stoakes (ARM)
@ 2026-07-27 18:10             ` Lorenzo Stoakes (ARM)
  0 siblings, 0 replies; 11+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-07-27 18:10 UTC (permalink / raw)
  To: David Hildenbrand (Arm)
  Cc: Hengbin Zhang, Andrew Morton, linux-mm, linux-kernel, stable,
	Zi Yan, Baolin Wang, Liam R. Howlett, Nico Pache, Ryan Roberts,
	Dev Jain, Barry Song, Lance Yang, Usama Arif

On Mon, Jul 27, 2026 at 07:09:10PM +0100, Lorenzo Stoakes (ARM) wrote:

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

Trailing sentence... tired :) delete 'so as per...'

>
> > >
> > > Suggested-by: David Hildenbrand <david@kernel.org>
>
> Should be a S-o-b and David should handle this.

Oops left this in by mistake (tired, late in UK :) this was when I was hoping I
could get David to handle it :P

Cheers, Lorenzo


^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2026-07-27 18:11 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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)
2026-07-27 18:10             ` Lorenzo Stoakes (ARM)

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox