mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-02-17 21:50:44 -05:00
Our goal is to move towards enabling vmalloc-huge by default on arm64 so
as to reduce TLB pressure. Therefore, we need a way to analyze the portion
of block mappings in vmalloc space we can get on a production system; this
can be done through ptdump, but currently we disable vmalloc-huge if
CONFIG_PTDUMP_DEBUGFS is on. The reason is that lazy freeing of kernel
pagetables via vmap_try_huge_pxd() may race with ptdump, so ptdump
may dereference a bogus address.
To solve this, we need to synchronize ptdump_walk() and ptdump_check_wx()
with pud_free_pmd_page() and pmd_free_pte_page().
Since this race is very unlikely to happen in practice, we do not want to
penalize the vmalloc pagetable tearing path by taking the init_mm
mmap_lock. Therefore, we use static keys. ptdump_walk() and
ptdump_check_wx() are the pagetable walkers; they will enable the static
key - upon observing that, the vmalloc pagetable tearing path will get
patched in with an mmap_read_lock/unlock sequence. A combination of the
patched-in mmap_read_lock/unlock, the acquire semantics of
static_branch_inc(), and the barriers in __flush_tlb_kernel_pgtable()
ensures that ptdump will never get a hold on the address of a freed PMD
or PTE table.
We can verify the correctness of the algorithm via the following litmus
test (thanks to James Houghton and Will Deacon):
AArch64 ptdump
Variant=Ifetch
{
uint64_t pud=0xa110c;
uint64_t pmd;
0:X0=label:"P1:L0"; 0:X1=instr:"NOP"; 0:X2=lock; 0:X3=pud; 0:X4=pmd;
1:X1=0xdead; 1:X2=lock; 1:X3=pud; 1:X4=pmd;
}
P0 | P1 ;
(* static_key_enable *) | (* pud_free_pmd_page *) ;
STR W1, [X0] | LDR X9, [X3] ;
DC CVAU,X0 | STR XZR, [X3] ;
DSB ISH | DSB ISH ;
IC IVAU,X0 | ISB ;
DSB ISH | ;
ISB | (* static key *) ;
| L0: ;
(* mmap_lock *) | B out1 ;
Lwlock: | ;
MOV W7, #1 | (* mmap_lock *) ;
SWPA W7, W8, [X2] | Lrlock: ;
| MOV W7, #1 ;
| SWPA W7, W8, [X2] ;
(* walk pgtable *) | ;
LDR X9, [X3] | (* mmap_unlock *) ;
CBZ X9, out0 | STLR WZR, [X2] ;
EOR X10, X9, X9 | ;
LDR X11, [X4, X10] | out1: ;
| EOR X10, X9, X9 ;
out0: | STR X1, [X4, X10] ;
exists (0:X8=0 /\ 1:X8=0 /\ (* Lock acquisitions succeed *)
0:X9=0xa110c /\ (* P0 sees the valid PUD ...*)
0:X11=0xdead) (* ... but the freed PMD *)
For an approximate written proof of why this algorithm works, please read
the code comment in [1], which is now removed for the sake of simplicity.
mm-selftests pass. No issues were observed while parallelly running
test_vmalloc.sh (which stresses the vmalloc subsystem),
and cat /sys/kernel/debug/{kernel_page_tables, check_wx_pages} in a loop.
Link: https://lore.kernel.org/all/20250723161827.15802-1-dev.jain@arm.com/ [1]
Reviewed-by: Ryan Roberts <ryan.roberts@arm.com>
Signed-off-by: Dev Jain <dev.jain@arm.com>
Signed-off-by: Will Deacon <will@kernel.org>
410 lines
9.5 KiB
C
410 lines
9.5 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Copyright (c) 2014, The Linux Foundation. All rights reserved.
|
|
* Debug helper to dump the current kernel pagetables of the system
|
|
* so that we can see what the various memory ranges are set to.
|
|
*
|
|
* Derived from x86 and arm implementation:
|
|
* (C) Copyright 2008 Intel Corporation
|
|
*
|
|
* Author: Arjan van de Ven <arjan@linux.intel.com>
|
|
*/
|
|
#include <linux/debugfs.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/io.h>
|
|
#include <linux/init.h>
|
|
#include <linux/mm.h>
|
|
#include <linux/ptdump.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/seq_file.h>
|
|
|
|
#include <asm/fixmap.h>
|
|
#include <asm/kasan.h>
|
|
#include <asm/memory.h>
|
|
#include <asm/pgtable-hwdef.h>
|
|
#include <asm/ptdump.h>
|
|
|
|
|
|
#define pt_dump_seq_printf(m, fmt, args...) \
|
|
({ \
|
|
if (m) \
|
|
seq_printf(m, fmt, ##args); \
|
|
})
|
|
|
|
#define pt_dump_seq_puts(m, fmt) \
|
|
({ \
|
|
if (m) \
|
|
seq_printf(m, fmt); \
|
|
})
|
|
|
|
static const struct ptdump_prot_bits pte_bits[] = {
|
|
{
|
|
.mask = PTE_VALID,
|
|
.val = PTE_VALID,
|
|
.set = " ",
|
|
.clear = "F",
|
|
}, {
|
|
.mask = PTE_USER,
|
|
.val = PTE_USER,
|
|
.set = "USR",
|
|
.clear = " ",
|
|
}, {
|
|
.mask = PTE_RDONLY,
|
|
.val = PTE_RDONLY,
|
|
.set = "ro",
|
|
.clear = "RW",
|
|
}, {
|
|
.mask = PTE_PXN,
|
|
.val = PTE_PXN,
|
|
.set = "NX",
|
|
.clear = "x ",
|
|
}, {
|
|
.mask = PTE_SHARED,
|
|
.val = PTE_SHARED,
|
|
.set = "SHD",
|
|
.clear = " ",
|
|
}, {
|
|
.mask = PTE_AF,
|
|
.val = PTE_AF,
|
|
.set = "AF",
|
|
.clear = " ",
|
|
}, {
|
|
.mask = PTE_NG,
|
|
.val = PTE_NG,
|
|
.set = "NG",
|
|
.clear = " ",
|
|
}, {
|
|
.mask = PTE_CONT,
|
|
.val = PTE_CONT,
|
|
.set = "CON",
|
|
.clear = " ",
|
|
}, {
|
|
.mask = PMD_TYPE_MASK,
|
|
.val = PMD_TYPE_SECT,
|
|
.set = "BLK",
|
|
.clear = " ",
|
|
}, {
|
|
.mask = PTE_UXN,
|
|
.val = PTE_UXN,
|
|
.set = "UXN",
|
|
.clear = " ",
|
|
}, {
|
|
.mask = PTE_GP,
|
|
.val = PTE_GP,
|
|
.set = "GP",
|
|
.clear = " ",
|
|
}, {
|
|
.mask = PTE_ATTRINDX_MASK,
|
|
.val = PTE_ATTRINDX(MT_DEVICE_nGnRnE),
|
|
.set = "DEVICE/nGnRnE",
|
|
}, {
|
|
.mask = PTE_ATTRINDX_MASK,
|
|
.val = PTE_ATTRINDX(MT_DEVICE_nGnRE),
|
|
.set = "DEVICE/nGnRE",
|
|
}, {
|
|
.mask = PTE_ATTRINDX_MASK,
|
|
.val = PTE_ATTRINDX(MT_NORMAL_NC),
|
|
.set = "MEM/NORMAL-NC",
|
|
}, {
|
|
.mask = PTE_ATTRINDX_MASK,
|
|
.val = PTE_ATTRINDX(MT_NORMAL),
|
|
.set = "MEM/NORMAL",
|
|
}, {
|
|
.mask = PTE_ATTRINDX_MASK,
|
|
.val = PTE_ATTRINDX(MT_NORMAL_TAGGED),
|
|
.set = "MEM/NORMAL-TAGGED",
|
|
}
|
|
};
|
|
|
|
static struct ptdump_pg_level kernel_pg_levels[] __ro_after_init = {
|
|
{ /* pgd */
|
|
.name = "PGD",
|
|
.bits = pte_bits,
|
|
.num = ARRAY_SIZE(pte_bits),
|
|
}, { /* p4d */
|
|
.name = "P4D",
|
|
.bits = pte_bits,
|
|
.num = ARRAY_SIZE(pte_bits),
|
|
}, { /* pud */
|
|
.name = "PUD",
|
|
.bits = pte_bits,
|
|
.num = ARRAY_SIZE(pte_bits),
|
|
}, { /* pmd */
|
|
.name = "PMD",
|
|
.bits = pte_bits,
|
|
.num = ARRAY_SIZE(pte_bits),
|
|
}, { /* pte */
|
|
.name = "PTE",
|
|
.bits = pte_bits,
|
|
.num = ARRAY_SIZE(pte_bits),
|
|
},
|
|
};
|
|
|
|
static void dump_prot(struct ptdump_pg_state *st, const struct ptdump_prot_bits *bits,
|
|
size_t num)
|
|
{
|
|
unsigned i;
|
|
|
|
for (i = 0; i < num; i++, bits++) {
|
|
const char *s;
|
|
|
|
if ((st->current_prot & bits->mask) == bits->val)
|
|
s = bits->set;
|
|
else
|
|
s = bits->clear;
|
|
|
|
if (s)
|
|
pt_dump_seq_printf(st->seq, " %s", s);
|
|
}
|
|
}
|
|
|
|
static void note_prot_uxn(struct ptdump_pg_state *st, unsigned long addr)
|
|
{
|
|
if (!st->check_wx)
|
|
return;
|
|
|
|
if ((st->current_prot & PTE_UXN) == PTE_UXN)
|
|
return;
|
|
|
|
WARN_ONCE(1, "arm64/mm: Found non-UXN mapping at address %p/%pS\n",
|
|
(void *)st->start_address, (void *)st->start_address);
|
|
|
|
st->uxn_pages += (addr - st->start_address) / PAGE_SIZE;
|
|
}
|
|
|
|
static void note_prot_wx(struct ptdump_pg_state *st, unsigned long addr)
|
|
{
|
|
if (!st->check_wx)
|
|
return;
|
|
if ((st->current_prot & PTE_RDONLY) == PTE_RDONLY)
|
|
return;
|
|
if ((st->current_prot & PTE_PXN) == PTE_PXN)
|
|
return;
|
|
|
|
WARN_ONCE(1, "arm64/mm: Found insecure W+X mapping at address %p/%pS\n",
|
|
(void *)st->start_address, (void *)st->start_address);
|
|
|
|
st->wx_pages += (addr - st->start_address) / PAGE_SIZE;
|
|
}
|
|
|
|
void note_page(struct ptdump_state *pt_st, unsigned long addr, int level,
|
|
pteval_t val)
|
|
{
|
|
struct ptdump_pg_state *st = container_of(pt_st, struct ptdump_pg_state, ptdump);
|
|
struct ptdump_pg_level *pg_level = st->pg_level;
|
|
static const char units[] = "KMGTPE";
|
|
ptdesc_t prot = 0;
|
|
|
|
/* check if the current level has been folded dynamically */
|
|
if (st->mm && ((level == 1 && mm_p4d_folded(st->mm)) ||
|
|
(level == 2 && mm_pud_folded(st->mm))))
|
|
level = 0;
|
|
|
|
if (level >= 0)
|
|
prot = val & pg_level[level].mask;
|
|
|
|
if (st->level == -1) {
|
|
st->level = level;
|
|
st->current_prot = prot;
|
|
st->start_address = addr;
|
|
pt_dump_seq_printf(st->seq, "---[ %s ]---\n", st->marker->name);
|
|
} else if (prot != st->current_prot || level != st->level ||
|
|
addr >= st->marker[1].start_address) {
|
|
const char *unit = units;
|
|
unsigned long delta;
|
|
|
|
if (st->current_prot) {
|
|
note_prot_uxn(st, addr);
|
|
note_prot_wx(st, addr);
|
|
}
|
|
|
|
pt_dump_seq_printf(st->seq, "0x%016lx-0x%016lx ",
|
|
st->start_address, addr);
|
|
|
|
delta = (addr - st->start_address) >> 10;
|
|
while (!(delta & 1023) && unit[1]) {
|
|
delta >>= 10;
|
|
unit++;
|
|
}
|
|
pt_dump_seq_printf(st->seq, "%9lu%c %s", delta, *unit,
|
|
pg_level[st->level].name);
|
|
if (st->current_prot && pg_level[st->level].bits)
|
|
dump_prot(st, pg_level[st->level].bits,
|
|
pg_level[st->level].num);
|
|
pt_dump_seq_puts(st->seq, "\n");
|
|
|
|
if (addr >= st->marker[1].start_address) {
|
|
st->marker++;
|
|
pt_dump_seq_printf(st->seq, "---[ %s ]---\n", st->marker->name);
|
|
}
|
|
|
|
st->start_address = addr;
|
|
st->current_prot = prot;
|
|
st->level = level;
|
|
}
|
|
|
|
if (addr >= st->marker[1].start_address) {
|
|
st->marker++;
|
|
pt_dump_seq_printf(st->seq, "---[ %s ]---\n", st->marker->name);
|
|
}
|
|
|
|
}
|
|
|
|
void note_page_pte(struct ptdump_state *pt_st, unsigned long addr, pte_t pte)
|
|
{
|
|
note_page(pt_st, addr, 4, pte_val(pte));
|
|
}
|
|
|
|
void note_page_pmd(struct ptdump_state *pt_st, unsigned long addr, pmd_t pmd)
|
|
{
|
|
note_page(pt_st, addr, 3, pmd_val(pmd));
|
|
}
|
|
|
|
void note_page_pud(struct ptdump_state *pt_st, unsigned long addr, pud_t pud)
|
|
{
|
|
note_page(pt_st, addr, 2, pud_val(pud));
|
|
}
|
|
|
|
void note_page_p4d(struct ptdump_state *pt_st, unsigned long addr, p4d_t p4d)
|
|
{
|
|
note_page(pt_st, addr, 1, p4d_val(p4d));
|
|
}
|
|
|
|
void note_page_pgd(struct ptdump_state *pt_st, unsigned long addr, pgd_t pgd)
|
|
{
|
|
note_page(pt_st, addr, 0, pgd_val(pgd));
|
|
}
|
|
|
|
void note_page_flush(struct ptdump_state *pt_st)
|
|
{
|
|
pte_t pte_zero = {0};
|
|
|
|
note_page(pt_st, 0, -1, pte_val(pte_zero));
|
|
}
|
|
|
|
static void arm64_ptdump_walk_pgd(struct ptdump_state *st, struct mm_struct *mm)
|
|
{
|
|
static_branch_inc(&arm64_ptdump_lock_key);
|
|
ptdump_walk_pgd(st, mm, NULL);
|
|
static_branch_dec(&arm64_ptdump_lock_key);
|
|
}
|
|
|
|
void ptdump_walk(struct seq_file *s, struct ptdump_info *info)
|
|
{
|
|
unsigned long end = ~0UL;
|
|
struct ptdump_pg_state st;
|
|
|
|
if (info->base_addr < TASK_SIZE_64)
|
|
end = TASK_SIZE_64;
|
|
|
|
st = (struct ptdump_pg_state){
|
|
.seq = s,
|
|
.marker = info->markers,
|
|
.mm = info->mm,
|
|
.pg_level = &kernel_pg_levels[0],
|
|
.level = -1,
|
|
.ptdump = {
|
|
.note_page_pte = note_page_pte,
|
|
.note_page_pmd = note_page_pmd,
|
|
.note_page_pud = note_page_pud,
|
|
.note_page_p4d = note_page_p4d,
|
|
.note_page_pgd = note_page_pgd,
|
|
.note_page_flush = note_page_flush,
|
|
.range = (struct ptdump_range[]){
|
|
{info->base_addr, end},
|
|
{0, 0}
|
|
}
|
|
}
|
|
};
|
|
|
|
arm64_ptdump_walk_pgd(&st.ptdump, info->mm);
|
|
}
|
|
|
|
static void __init ptdump_initialize(void)
|
|
{
|
|
unsigned i, j;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(kernel_pg_levels); i++)
|
|
if (kernel_pg_levels[i].bits)
|
|
for (j = 0; j < kernel_pg_levels[i].num; j++)
|
|
kernel_pg_levels[i].mask |= kernel_pg_levels[i].bits[j].mask;
|
|
}
|
|
|
|
static struct ptdump_info kernel_ptdump_info __ro_after_init = {
|
|
.mm = &init_mm,
|
|
};
|
|
|
|
bool ptdump_check_wx(void)
|
|
{
|
|
struct ptdump_pg_state st = {
|
|
.seq = NULL,
|
|
.marker = (struct addr_marker[]) {
|
|
{ 0, NULL},
|
|
{ -1, NULL},
|
|
},
|
|
.pg_level = &kernel_pg_levels[0],
|
|
.level = -1,
|
|
.check_wx = true,
|
|
.ptdump = {
|
|
.note_page_pte = note_page_pte,
|
|
.note_page_pmd = note_page_pmd,
|
|
.note_page_pud = note_page_pud,
|
|
.note_page_p4d = note_page_p4d,
|
|
.note_page_pgd = note_page_pgd,
|
|
.note_page_flush = note_page_flush,
|
|
.range = (struct ptdump_range[]) {
|
|
{_PAGE_OFFSET(vabits_actual), ~0UL},
|
|
{0, 0}
|
|
}
|
|
}
|
|
};
|
|
|
|
arm64_ptdump_walk_pgd(&st.ptdump, &init_mm);
|
|
|
|
if (st.wx_pages || st.uxn_pages) {
|
|
pr_warn("Checked W+X mappings: FAILED, %lu W+X pages found, %lu non-UXN pages found\n",
|
|
st.wx_pages, st.uxn_pages);
|
|
|
|
return false;
|
|
} else {
|
|
pr_info("Checked W+X mappings: passed, no W+X pages found\n");
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static int __init ptdump_init(void)
|
|
{
|
|
u64 page_offset = _PAGE_OFFSET(vabits_actual);
|
|
u64 vmemmap_start = (u64)virt_to_page((void *)page_offset);
|
|
struct addr_marker m[] = {
|
|
{ PAGE_OFFSET, "Linear Mapping start" },
|
|
{ PAGE_END, "Linear Mapping end" },
|
|
#if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)
|
|
{ KASAN_SHADOW_START, "Kasan shadow start" },
|
|
{ KASAN_SHADOW_END, "Kasan shadow end" },
|
|
#endif
|
|
{ MODULES_VADDR, "Modules start" },
|
|
{ MODULES_END, "Modules end" },
|
|
{ VMALLOC_START, "vmalloc() area" },
|
|
{ VMALLOC_END, "vmalloc() end" },
|
|
{ vmemmap_start, "vmemmap start" },
|
|
{ VMEMMAP_END, "vmemmap end" },
|
|
{ PCI_IO_START, "PCI I/O start" },
|
|
{ PCI_IO_END, "PCI I/O end" },
|
|
{ FIXADDR_TOT_START, "Fixmap start" },
|
|
{ FIXADDR_TOP, "Fixmap end" },
|
|
{ -1, NULL },
|
|
};
|
|
static struct addr_marker address_markers[ARRAY_SIZE(m)] __ro_after_init;
|
|
|
|
kernel_ptdump_info.markers = memcpy(address_markers, m, sizeof(m));
|
|
kernel_ptdump_info.base_addr = page_offset;
|
|
|
|
ptdump_initialize();
|
|
ptdump_debugfs_register(&kernel_ptdump_info, "kernel_page_tables");
|
|
return 0;
|
|
}
|
|
device_initcall(ptdump_init);
|