]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
arm: pgtable: fix NULL pointer dereference issue
authorQi Zheng <zhengqi.arch@bytedance.com>
Mon, 17 Feb 2025 02:49:24 +0000 (10:49 +0800)
committerAndrew Morton <akpm@linux-foundation.org>
Thu, 6 Mar 2025 05:36:12 +0000 (21:36 -0800)
When update_mmu_cache_range() is called by update_mmu_cache(), the vmf
parameter is NULL, which will cause a NULL pointer dereference issue in
adjust_pte():

Unable to handle kernel NULL pointer dereference at virtual address 00000030 when read
Hardware name: Atmel AT91SAM9
PC is at update_mmu_cache_range+0x1e0/0x278
LR is at pte_offset_map_rw_nolock+0x18/0x2c
Call trace:
 update_mmu_cache_range from remove_migration_pte+0x29c/0x2ec
 remove_migration_pte from rmap_walk_file+0xcc/0x130
 rmap_walk_file from remove_migration_ptes+0x90/0xa4
 remove_migration_ptes from migrate_pages_batch+0x6d4/0x858
 migrate_pages_batch from migrate_pages+0x188/0x488
 migrate_pages from compact_zone+0x56c/0x954
 compact_zone from compact_node+0x90/0xf0
 compact_node from kcompactd+0x1d4/0x204
 kcompactd from kthread+0x120/0x12c
 kthread from ret_from_fork+0x14/0x38
Exception stack(0xc0d8bfb0 to 0xc0d8bff8)

To fix it, do not rely on whether 'ptl' is equal to decide whether to hold
the pte lock, but decide it by whether CONFIG_SPLIT_PTE_PTLOCKS is
enabled.  In addition, if two vmas map to the same PTE page, there is no
need to hold the pte lock again, otherwise a deadlock will occur.  Just
add the need_lock parameter to let adjust_pte() know this information.

Link: https://lkml.kernel.org/r/20250217024924.57996-1-zhengqi.arch@bytedance.com
Fixes: fc9c45b71f43 ("arm: adjust_pte() use pte_offset_map_rw_nolock()")
Signed-off-by: Qi Zheng <zhengqi.arch@bytedance.com>
Reported-by: Ezra Buehler <ezra.buehler@husqvarnagroup.com>
Closes: https://lore.kernel.org/lkml/CAM1KZSmZ2T_riHvay+7cKEFxoPgeVpHkVFTzVVEQ1BO0cLkHEQ@mail.gmail.com/
Acked-by: David Hildenbrand <david@redhat.com>
Tested-by: Ezra Buehler <ezra.buehler@husqvarnagroup.com>
Cc: Hugh Dickins <hughd@google.com>
Cc: Muchun Song <muchun.song@linux.dev>
Cc: Qi Zheng <zhengqi.arch@bytedance.com>
Cc: Russel King <linux@armlinux.org.uk>
Cc: Ryan Roberts <ryan.roberts@arm.com>
Cc: <stable@vger.kernel.org>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
arch/arm/mm/fault-armv.c

index 2bec87c3327d23dd39b6400a46fa1f5cccd812a7..39fd5df73317856d087ba63410e37810ee0dde90 100644 (file)
@@ -62,7 +62,7 @@ static int do_adjust_pte(struct vm_area_struct *vma, unsigned long address,
 }
 
 static int adjust_pte(struct vm_area_struct *vma, unsigned long address,
-                     unsigned long pfn, struct vm_fault *vmf)
+                     unsigned long pfn, bool need_lock)
 {
        spinlock_t *ptl;
        pgd_t *pgd;
@@ -99,12 +99,11 @@ again:
        if (!pte)
                return 0;
 
-       /*
-        * If we are using split PTE locks, then we need to take the page
-        * lock here.  Otherwise we are using shared mm->page_table_lock
-        * which is already locked, thus cannot take it.
-        */
-       if (ptl != vmf->ptl) {
+       if (need_lock) {
+               /*
+                * Use nested version here to indicate that we are already
+                * holding one similar spinlock.
+                */
                spin_lock_nested(ptl, SINGLE_DEPTH_NESTING);
                if (unlikely(!pmd_same(pmdval, pmdp_get_lockless(pmd)))) {
                        pte_unmap_unlock(pte, ptl);
@@ -114,7 +113,7 @@ again:
 
        ret = do_adjust_pte(vma, address, pfn, pte);
 
-       if (ptl != vmf->ptl)
+       if (need_lock)
                spin_unlock(ptl);
        pte_unmap(pte);
 
@@ -123,9 +122,10 @@ again:
 
 static void
 make_coherent(struct address_space *mapping, struct vm_area_struct *vma,
-             unsigned long addr, pte_t *ptep, unsigned long pfn,
-             struct vm_fault *vmf)
+             unsigned long addr, pte_t *ptep, unsigned long pfn)
 {
+       const unsigned long pmd_start_addr = ALIGN_DOWN(addr, PMD_SIZE);
+       const unsigned long pmd_end_addr = pmd_start_addr + PMD_SIZE;
        struct mm_struct *mm = vma->vm_mm;
        struct vm_area_struct *mpnt;
        unsigned long offset;
@@ -141,6 +141,14 @@ make_coherent(struct address_space *mapping, struct vm_area_struct *vma,
         */
        flush_dcache_mmap_lock(mapping);
        vma_interval_tree_foreach(mpnt, &mapping->i_mmap, pgoff, pgoff) {
+               /*
+                * If we are using split PTE locks, then we need to take the pte
+                * lock. Otherwise we are using shared mm->page_table_lock which
+                * is already locked, thus cannot take it.
+                */
+               bool need_lock = IS_ENABLED(CONFIG_SPLIT_PTE_PTLOCKS);
+               unsigned long mpnt_addr;
+
                /*
                 * If this VMA is not in our MM, we can ignore it.
                 * Note that we intentionally mask out the VMA
@@ -151,7 +159,12 @@ make_coherent(struct address_space *mapping, struct vm_area_struct *vma,
                if (!(mpnt->vm_flags & VM_MAYSHARE))
                        continue;
                offset = (pgoff - mpnt->vm_pgoff) << PAGE_SHIFT;
-               aliases += adjust_pte(mpnt, mpnt->vm_start + offset, pfn, vmf);
+               mpnt_addr = mpnt->vm_start + offset;
+
+               /* Avoid deadlocks by not grabbing the same PTE lock again. */
+               if (mpnt_addr >= pmd_start_addr && mpnt_addr < pmd_end_addr)
+                       need_lock = false;
+               aliases += adjust_pte(mpnt, mpnt_addr, pfn, need_lock);
        }
        flush_dcache_mmap_unlock(mapping);
        if (aliases)
@@ -194,7 +207,7 @@ void update_mmu_cache_range(struct vm_fault *vmf, struct vm_area_struct *vma,
                __flush_dcache_folio(mapping, folio);
        if (mapping) {
                if (cache_is_vivt())
-                       make_coherent(mapping, vma, addr, ptep, pfn, vmf);
+                       make_coherent(mapping, vma, addr, ptep, pfn);
                else if (vma->vm_flags & VM_EXEC)
                        __flush_icache_all();
        }