]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
mm,hugetlb: change mechanism to detect a COW on private mapping
authorOscar Salvador <osalvador@suse.de>
Mon, 30 Jun 2025 14:42:08 +0000 (16:42 +0200)
committerAndrew Morton <akpm@linux-foundation.org>
Sun, 13 Jul 2025 23:38:22 +0000 (16:38 -0700)
Patch series "Misc rework on hugetlb faulting path", v4.

This patchset aims to give some love to the hugetlb faulting path, doing
so by removing obsolete comments that are no longer true, sorting out the
folio lock, and changing the mechanism we use to determine whether we are
COWing a private mapping already.

The most important patch of the series is #1, as it fixes a deadlock that
was described in [1], where two processes were holding the same lock for
the folio in the pagecache, and then deadlocked in the mutex.  Note that
this can also happen for anymous folios.  This has been tested using this
reproducer, below

Looking up and locking the folio in the pagecache was done to check
whether that folio was the same folio we had mapped in our pagetables,
meaning that if it was different we knew that we already mapped that folio
privately, so any further CoW would be made on a private mapping, which
lead us to the question: __Was the reservation for that address
consumed?__ That is all we care about, because if it was indeed consumed
and we are the owner and we cannot allocate more folios, we need to unmap
the folio from the processes pagetables and make it exclusive for us.

We figured we do not need to look up the folio at all, and it is just
enough to check whether the folio we have mapped is anonymous, which means
we mapped it privately, so the reservation was indeed consumed.

Patch#2 sorts out folio locking in the faulting path, reducing the scope
of it ,only taking it when we are dealing with an anonymous folio and
document it.  More details in the patch.

Patch#3-5 are cleanups.

Here is the reproducer:

 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <sys/mman.h>
 #include <sys/wait.h>

 #define PROTECTION (PROT_READ | PROT_WRITE)
 #define LENGTH (2UL*1024*1024)

 #define ADDR (void *)(0x0UL)
 #define FLAGS (MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB)

 void __read(char *addr)
 {
  int i = 0;

  printf("a[%d]: %c\n", i, addr[i]);
 }

 void fill(char *addr)
 {
  addr[0] = 'd';

  printf("addr: %c\n", addr[0]);
 }

 int main(void)
 {
  void *addr;
  pid_t pid, wpid;
  int status;

  addr = mmap(ADDR, LENGTH, PROTECTION, FLAGS, -1, 0);
  if (addr == MAP_FAILED) {
  perror("mmap");
  return -1;
  }

  printf("Parent faulting in RO\n");
  __read(addr);

  sleep (10);
  printf("Forking\n");

  pid = fork();
  switch (pid) {
  case -1:
  perror("fork");
  break;
  case 0:
  sleep (4);
  printf("Child: Faulting in\n");
  fill(addr);
  exit(0);
  break;
  default:
  printf("Parent: Faulting in\n");
  fill(addr);
  while((wpid = wait(&status)) > 0);
  if (munmap(addr, LENGTH))
  perror("munmap");
  }

  return 0;
 }

You will also have to add a delay in hugetlb_wp, after releasing the mutex
and before unmapping, so the window is large enough to reproduce it
reliably.

:  --- a/mm/hugetlb.c
:  +++ b/mm/hugetlb.c
:  @@ -38,6 +38,7 @@
:   #include <linux/memory.h>
:   #include <linux/mm_inline.h>
:   #include <linux/padata.h>
:  +#include <linux/delay.h>
:
:   #include <asm/page.h>
:   #include <asm/pgalloc.h>
:  @@ -6261,6 +6262,8 @@ static vm_fault_t hugetlb_wp(struct vm_fault *vmf)
:    hugetlb_vma_unlock_read(vma);
:    mutex_unlock(&hugetlb_fault_mutex_table[hash]);
:
:  + mdelay(8000);
:  +
:    unmap_ref_private(mm, vma, old_folio, vmf->address);
:
:    mutex_lock(&hugetlb_fault_mutex_table[hash]);

This patch (of 5):

hugetlb_wp() checks whether the process is trying to COW on a private
mapping in order to know whether the reservation for that address was
already consumed.  If it was consumed and we are the ownner of the
mapping, the folio will have to be unmapped from the other processes.

Currently, that check is done by looking up the folio in the pagecache and
compare it to the folio which is mapped in our pagetables.  If it differs,
it means we already mapped it privately before, consuming a reservation on
the way.  All we are interested in is whether the mapped folio is
anonymous, so we can simplify and check for that instead.

Link: https://lkml.kernel.org/r/20250630144212.156938-1-osalvador@suse.de
Link: https://lkml.kernel.org/r/20250627102904.107202-1-osalvador@suse.de
Link: https://lkml.kernel.org/r/20250627102904.107202-2-osalvador@suse.de
Link: https://lore.kernel.org/lkml/20250513093448.592150-1-gavinguo@igalia.com/
Link: https://lkml.kernel.org/r/20250630144212.156938-2-osalvador@suse.de
Fixes: 40549ba8f8e0 ("hugetlb: use new vma_lock for pmd sharing synchronization")
Signed-off-by: Oscar Salvador <osalvador@suse.de>
Reported-by: Gavin Guo <gavinguo@igalia.com>
Closes: https://lore.kernel.org/lkml/20250513093448.592150-1-gavinguo@igalia.com/
Suggested-by: Peter Xu <peterx@redhat.com>
Acked-by: David Hildenbrand <david@redhat.com>
Cc: Muchun Song <muchun.song@linux.dev>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
mm/hugetlb.c

index db53ead8ac4340a7e8d5f19d0ac41751b941b1ea..cf5d5ad5bbe9dedfe76d6506e04f558d955b86ae 100644 (file)
@@ -6131,8 +6131,7 @@ static void unmap_ref_private(struct mm_struct *mm, struct vm_area_struct *vma,
  * cannot race with other handlers or page migration.
  * Keep the pte_same checks anyway to make transition from the mutex easier.
  */
-static vm_fault_t hugetlb_wp(struct folio *pagecache_folio,
-                      struct vm_fault *vmf)
+static vm_fault_t hugetlb_wp(struct vm_fault *vmf)
 {
        struct vm_area_struct *vma = vmf->vma;
        struct mm_struct *mm = vma->vm_mm;
@@ -6194,16 +6193,17 @@ retry_avoidcopy:
                       PageAnonExclusive(&old_folio->page), &old_folio->page);
 
        /*
-        * If the process that created a MAP_PRIVATE mapping is about to
-        * perform a COW due to a shared page count, attempt to satisfy
-        * the allocation without using the existing reserves. The pagecache
-        * page is used to determine if the reserve at this address was
-        * consumed or not. If reserves were used, a partial faulted mapping
-        * at the time of fork() could consume its reserves on COW instead
-        * of the full address range.
+        * If the process that created a MAP_PRIVATE mapping is about to perform
+        * a COW due to a shared page count, attempt to satisfy the allocation
+        * without using the existing reserves.
+        * In order to determine where this is a COW on a MAP_PRIVATE mapping it
+        * is enough to check whether the old_folio is anonymous. This means that
+        * the reserve for this address was consumed. If reserves were used, a
+        * partial faulted mapping at the fime of fork() could consume its reserves
+        * on COW instead of the full address range.
         */
        if (is_vma_resv_set(vma, HPAGE_RESV_OWNER) &&
-                       old_folio != pagecache_folio)
+           folio_test_anon(old_folio))
                cow_from_owner = true;
 
        folio_get(old_folio);
@@ -6582,7 +6582,7 @@ static vm_fault_t hugetlb_no_page(struct address_space *mapping,
        hugetlb_count_add(pages_per_huge_page(h), mm);
        if ((vmf->flags & FAULT_FLAG_WRITE) && !(vma->vm_flags & VM_SHARED)) {
                /* Optimization, do the COW without a second fault */
-               ret = hugetlb_wp(folio, vmf);
+               ret = hugetlb_wp(vmf);
        }
 
        spin_unlock(vmf->ptl);
@@ -6650,10 +6650,9 @@ vm_fault_t hugetlb_fault(struct mm_struct *mm, struct vm_area_struct *vma,
        vm_fault_t ret;
        u32 hash;
        struct folio *folio = NULL;
-       struct folio *pagecache_folio = NULL;
        struct hstate *h = hstate_vma(vma);
        struct address_space *mapping;
-       int need_wait_lock = 0;
+       bool need_wait_lock = false;
        struct vm_fault vmf = {
                .vma = vma,
                .address = address & huge_page_mask(h),
@@ -6748,8 +6747,7 @@ vm_fault_t hugetlb_fault(struct mm_struct *mm, struct vm_area_struct *vma,
         * If we are going to COW/unshare the mapping later, we examine the
         * pending reservations for this page now. This will ensure that any
         * allocations necessary to record that reservation occur outside the
-        * spinlock. Also lookup the pagecache page now as it is used to
-        * determine if a reservation has been consumed.
+        * spinlock.
         */
        if ((flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) &&
            !(vma->vm_flags & VM_MAYSHARE) && !huge_pte_write(vmf.orig_pte)) {
@@ -6759,11 +6757,6 @@ vm_fault_t hugetlb_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                }
                /* Just decrements count, does not deallocate */
                vma_end_reservation(h, vma, vmf.address);
-
-               pagecache_folio = filemap_lock_hugetlb_folio(h, mapping,
-                                                            vmf.pgoff);
-               if (IS_ERR(pagecache_folio))
-                       pagecache_folio = NULL;
        }
 
        vmf.ptl = huge_pte_lock(h, mm, vmf.pte);
@@ -6777,10 +6770,6 @@ vm_fault_t hugetlb_fault(struct mm_struct *mm, struct vm_area_struct *vma,
            (flags & FAULT_FLAG_WRITE) && !huge_pte_write(vmf.orig_pte)) {
                if (!userfaultfd_wp_async(vma)) {
                        spin_unlock(vmf.ptl);
-                       if (pagecache_folio) {
-                               folio_unlock(pagecache_folio);
-                               folio_put(pagecache_folio);
-                       }
                        hugetlb_vma_unlock_read(vma);
                        mutex_unlock(&hugetlb_fault_mutex_table[hash]);
                        return handle_userfault(&vmf, VM_UFFD_WP);
@@ -6792,24 +6781,19 @@ vm_fault_t hugetlb_fault(struct mm_struct *mm, struct vm_area_struct *vma,
                /* Fallthrough to CoW */
        }
 
-       /*
-        * hugetlb_wp() requires page locks of pte_page(vmf.orig_pte) and
-        * pagecache_folio, so here we need take the former one
-        * when folio != pagecache_folio or !pagecache_folio.
-        */
-       folio = page_folio(pte_page(vmf.orig_pte));
-       if (folio != pagecache_folio)
-               if (!folio_trylock(folio)) {
-                       need_wait_lock = 1;
-                       goto out_ptl;
-               }
-
-       folio_get(folio);
-
        if (flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) {
                if (!huge_pte_write(vmf.orig_pte)) {
-                       ret = hugetlb_wp(pagecache_folio, &vmf);
-                       goto out_put_page;
+                       /* hugetlb_wp() requires page locks of pte_page(vmf.orig_pte) */
+                       folio = page_folio(pte_page(vmf.orig_pte));
+                       if (!folio_trylock(folio)) {
+                               need_wait_lock = true;
+                               goto out_ptl;
+                       }
+                       folio_get(folio);
+                       ret = hugetlb_wp(&vmf);
+                       folio_unlock(folio);
+                       folio_put(folio);
+                       goto out_ptl;
                } else if (likely(flags & FAULT_FLAG_WRITE)) {
                        vmf.orig_pte = huge_pte_mkdirty(vmf.orig_pte);
                }
@@ -6818,17 +6802,8 @@ vm_fault_t hugetlb_fault(struct mm_struct *mm, struct vm_area_struct *vma,
        if (huge_ptep_set_access_flags(vma, vmf.address, vmf.pte, vmf.orig_pte,
                                                flags & FAULT_FLAG_WRITE))
                update_mmu_cache(vma, vmf.address, vmf.pte);
-out_put_page:
-       if (folio != pagecache_folio)
-               folio_unlock(folio);
-       folio_put(folio);
 out_ptl:
        spin_unlock(vmf.ptl);
-
-       if (pagecache_folio) {
-               folio_unlock(pagecache_folio);
-               folio_put(pagecache_folio);
-       }
 out_mutex:
        hugetlb_vma_unlock_read(vma);
 
@@ -6841,11 +6816,16 @@ out_mutex:
 
        mutex_unlock(&hugetlb_fault_mutex_table[hash]);
        /*
-        * Generally it's safe to hold refcount during waiting page lock. But
-        * here we just wait to defer the next page fault to avoid busy loop and
-        * the page is not used after unlocked before returning from the current
-        * page fault. So we are safe from accessing freed page, even if we wait
-        * here without taking refcount.
+        * hugetlb_wp drops all the locks, but the folio lock, before trying to
+        * unmap the folio from other processes. During that window, if another
+        * process mapping that folio faults in, it will take the mutex and then
+        * it will wait on folio_lock, causing an ABBA deadlock.
+        * Use trylock instead and bail out if we fail.
+        *
+        * Ideally, we should hold a refcount on the folio we wait for, but we do
+        * not want to use the folio after it becomes unlocked, but rather just
+        * wait for it to become unlocked, so hopefully next fault successes on
+        * the trylock.
         */
        if (need_wait_lock)
                folio_wait_locked(folio);