print_bad_pte() looks like something that should actually be a WARN
or similar, but historically it apparently has proven to be useful to
detect corruption of page tables even on production systems -- report
the issue and keep the system running to make it easier to actually detect
what is going wrong (e.g., multiple such messages might shed a light).

As we want to unify vm_normal_page_*() handling for PTE/PMD/PUD, we'll have
to take care of print_bad_pte() as well.

Let's prepare for using print_bad_pte() also for non-PTEs by adjusting the
implementation and renaming the function to print_bad_page_map().
Provide print_bad_pte() as a simple wrapper.

Document the implicit locking requirements for the page table re-walk.

To make the function a bit more readable, factor out the ratelimit check
into is_bad_page_map_ratelimited() and place the printing of page
table content into __print_bad_page_map_pgtable(). We'll now dump
information from each level in a single line, and just stop the table
walk once we hit something that is not a present page table.

The report will now look something like (dumping pgd to pmd values):

[   77.943408] BUG: Bad page map in process XXX  pte:80000001233f5867
[   77.944077] addr:00007fd84bb1c000 vm_flags:08100071 anon_vma: ...
[   77.945186] pgd:10a89f067 p4d:10a89f067 pud:10e5a2067 pmd:105327067

Not using pgdp_get(), because that does not work properly on some arm
configs where pgd_t is an array. Note that we are dumping all levels
even when levels are folded for simplicity.

Signed-off-by: David Hildenbrand <da...@redhat.com>
---
 include/linux/pgtable.h |  19 ++++++++
 mm/memory.c             | 104 ++++++++++++++++++++++++++++++++--------
 2 files changed, 103 insertions(+), 20 deletions(-)

diff --git a/include/linux/pgtable.h b/include/linux/pgtable.h
index bff5c4241bf2e..33c84b38b7ec6 100644
--- a/include/linux/pgtable.h
+++ b/include/linux/pgtable.h
@@ -1966,6 +1966,25 @@ enum pgtable_level {
        PGTABLE_LEVEL_PGD,
 };
 
+static inline const char *pgtable_level_to_str(enum pgtable_level level)
+{
+       switch (level) {
+       case PGTABLE_LEVEL_PTE:
+               return "pte";
+       case PGTABLE_LEVEL_PMD:
+               return "pmd";
+       case PGTABLE_LEVEL_PUD:
+               return "pud";
+       case PGTABLE_LEVEL_P4D:
+               return "p4d";
+       case PGTABLE_LEVEL_PGD:
+               return "pgd";
+       default:
+               VM_WARN_ON_ONCE(1);
+               return "unknown";
+       }
+}
+
 #endif /* !__ASSEMBLY__ */
 
 #if !defined(MAX_POSSIBLE_PHYSMEM_BITS) && !defined(CONFIG_64BIT)
diff --git a/mm/memory.c b/mm/memory.c
index 626caedce35e0..dc0107354d37b 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -491,22 +491,8 @@ static inline void add_mm_rss_vec(struct mm_struct *mm, 
int *rss)
                        add_mm_counter(mm, i, rss[i]);
 }
 
-/*
- * This function is called to print an error when a bad pte
- * is found. For example, we might have a PFN-mapped pte in
- * a region that doesn't allow it.
- *
- * The calling function must still handle the error.
- */
-static void print_bad_pte(struct vm_area_struct *vma, unsigned long addr,
-                         pte_t pte, struct page *page)
+static bool is_bad_page_map_ratelimited(void)
 {
-       pgd_t *pgd = pgd_offset(vma->vm_mm, addr);
-       p4d_t *p4d = p4d_offset(pgd, addr);
-       pud_t *pud = pud_offset(p4d, addr);
-       pmd_t *pmd = pmd_offset(pud, addr);
-       struct address_space *mapping;
-       pgoff_t index;
        static unsigned long resume;
        static unsigned long nr_shown;
        static unsigned long nr_unshown;
@@ -518,7 +504,7 @@ static void print_bad_pte(struct vm_area_struct *vma, 
unsigned long addr,
        if (nr_shown == 60) {
                if (time_before(jiffies, resume)) {
                        nr_unshown++;
-                       return;
+                       return true;
                }
                if (nr_unshown) {
                        pr_alert("BUG: Bad page map: %lu messages suppressed\n",
@@ -529,15 +515,91 @@ static void print_bad_pte(struct vm_area_struct *vma, 
unsigned long addr,
        }
        if (nr_shown++ == 0)
                resume = jiffies + 60 * HZ;
+       return false;
+}
+
+static void __print_bad_page_map_pgtable(struct mm_struct *mm, unsigned long 
addr)
+{
+       unsigned long long pgdv, p4dv, pudv, pmdv;
+       p4d_t p4d, *p4dp;
+       pud_t pud, *pudp;
+       pmd_t pmd, *pmdp;
+       pgd_t *pgdp;
+
+       /*
+        * Although this looks like a fully lockless pgtable walk, it is not:
+        * see locking requirements for print_bad_page_map().
+        */
+       pgdp = pgd_offset(mm, addr);
+       pgdv = pgd_val(*pgdp);
+
+       if (!pgd_present(*pgdp) || pgd_leaf(*pgdp)) {
+               pr_alert("pgd:%08llx\n", pgdv);
+               return;
+       }
+
+       p4dp = p4d_offset(pgdp, addr);
+       p4d = p4dp_get(p4dp);
+       p4dv = p4d_val(p4d);
+
+       if (!p4d_present(p4d) || p4d_leaf(p4d)) {
+               pr_alert("pgd:%08llx p4d:%08llx\n", pgdv, p4dv);
+               return;
+       }
+
+       pudp = pud_offset(p4dp, addr);
+       pud = pudp_get(pudp);
+       pudv = pud_val(pud);
+
+       if (!pud_present(pud) || pud_leaf(pud)) {
+               pr_alert("pgd:%08llx p4d:%08llx pud:%08llx\n", pgdv, p4dv, 
pudv);
+               return;
+       }
+
+       pmdp = pmd_offset(pudp, addr);
+       pmd = pmdp_get(pmdp);
+       pmdv = pmd_val(pmd);
+
+       /*
+        * Dumping the PTE would be nice, but it's tricky with CONFIG_HIGHPTE,
+        * because the table should already be mapped by the caller and
+        * doing another map would be bad. print_bad_page_map() should
+        * already take care of printing the PTE.
+        */
+       pr_alert("pgd:%08llx p4d:%08llx pud:%08llx pmd:%08llx\n", pgdv,
+                p4dv, pudv, pmdv);
+}
+
+/*
+ * This function is called to print an error when a bad page table entry (e.g.,
+ * corrupted page table entry) is found. For example, we might have a
+ * PFN-mapped pte in a region that doesn't allow it.
+ *
+ * The calling function must still handle the error.
+ *
+ * This function must be called during a proper page table walk, as it will
+ * re-walk the page table to dump information: the caller MUST prevent page
+ * table teardown (by holding mmap, vma or rmap lock) and MUST hold the leaf
+ * page table lock.
+ */
+static void print_bad_page_map(struct vm_area_struct *vma,
+               unsigned long addr, unsigned long long entry, struct page *page,
+               enum pgtable_level level)
+{
+       struct address_space *mapping;
+       pgoff_t index;
+
+       if (is_bad_page_map_ratelimited())
+               return;
 
        mapping = vma->vm_file ? vma->vm_file->f_mapping : NULL;
        index = linear_page_index(vma, addr);
 
-       pr_alert("BUG: Bad page map in process %s  pte:%08llx pmd:%08llx\n",
-                current->comm,
-                (long long)pte_val(pte), (long long)pmd_val(*pmd));
+       pr_alert("BUG: Bad page map in process %s  %s:%08llx", current->comm,
+                pgtable_level_to_str(level), entry);
+       __print_bad_page_map_pgtable(vma->vm_mm, addr);
        if (page)
-               dump_page(page, "bad pte");
+               dump_page(page, "bad page map");
        pr_alert("addr:%px vm_flags:%08lx anon_vma:%px mapping:%px index:%lx\n",
                 (void *)addr, vma->vm_flags, vma->anon_vma, mapping, index);
        pr_alert("file:%pD fault:%ps mmap:%ps mmap_prepare: %ps 
read_folio:%ps\n",
@@ -549,6 +611,8 @@ static void print_bad_pte(struct vm_area_struct *vma, 
unsigned long addr,
        dump_stack();
        add_taint(TAINT_BAD_PAGE, LOCKDEP_NOW_UNRELIABLE);
 }
+#define print_bad_pte(vma, addr, pte, page) \
+       print_bad_page_map(vma, addr, pte_val(pte), page, PGTABLE_LEVEL_PTE)
 
 /*
  * vm_normal_page -- This function gets the "struct page" associated with a 
pte.
-- 
2.50.1


Reply via email to