blob: 9a179d6b8ec591fa93c62c0de1a02e4ee4f889d8 [file] [log] [blame]
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* s390x MMU
*
* Copyright (c) 2017 Red Hat Inc
*
* Authors:
* David Hildenbrand <david@redhat.com>
*/
#include <libcflat.h>
#include <asm/pgtable.h>
#include <asm/arch_def.h>
#include <asm/barrier.h>
#include <asm/interrupt.h>
#include <vmalloc.h>
#include "mmu.h"
/*
* The naming convention used here is the same as used in the Linux kernel;
* this is the correspondence between the s390x architectural names and the
* Linux ones:
*
* pgd - region 1 table entry
* p4d - region 2 table entry
* pud - region 3 table entry
* pmd - segment table entry
* pte - page table entry
*/
static pgd_t *table_root;
static void mmu_enable(pgd_t *pgtable)
{
const uint64_t asce = __pa(pgtable) | ASCE_DT_REGION1 |
REGION_TABLE_LENGTH;
/* set primary asce */
lctlg(1, asce);
assert(stctg(1) == asce);
/* enable dat (primary == 0 set as default) */
enable_dat();
/* we can now also use DAT in all interrupt handlers */
irq_set_dat_mode(true, AS_PRIM);
}
/*
* Get the pud (region 3) DAT table entry for the given address and root,
* allocating it if necessary
*/
static inline pud_t *get_pud(pgd_t *pgtable, uintptr_t vaddr)
{
pgd_t *pgd = pgd_offset(pgtable, vaddr);
p4d_t *p4d = p4d_alloc(pgd, vaddr);
pud_t *pud = pud_alloc(p4d, vaddr);
return pud;
}
/*
* Get the pmd (segment) DAT table entry for the given address and pud,
* allocating it if necessary.
* The pud must not be huge.
*/
static inline pmd_t *get_pmd(pud_t *pud, uintptr_t vaddr)
{
pmd_t *pmd;
assert(!pud_huge(*pud));
pmd = pmd_alloc(pud, vaddr);
return pmd;
}
/*
* Get the pte (page) DAT table entry for the given address and pmd,
* allocating it if necessary.
* The pmd must not be large.
*/
static inline pte_t *get_pte(pmd_t *pmd, uintptr_t vaddr)
{
pte_t *pte;
assert(!pmd_large(*pmd));
pte = pte_alloc(pmd, vaddr);
return pte;
}
/*
* Splits a large pmd (segment) DAT table entry into equivalent 4kB small
* pages.
* @pmd The pmd to split, it must be large.
* @va the virtual address corresponding to this pmd.
*/
static void split_pmd(pmd_t *pmd, uintptr_t va)
{
phys_addr_t pa = pmd_val(*pmd) & SEGMENT_ENTRY_SFAA;
unsigned long i, prot;
pte_t *pte;
assert(pmd_large(*pmd));
pte = alloc_pages(PAGE_TABLE_ORDER);
prot = pmd_val(*pmd) & (SEGMENT_ENTRY_IEP | SEGMENT_ENTRY_P);
for (i = 0; i < PAGE_TABLE_ENTRIES; i++)
pte_val(pte[i]) = pa | PAGE_SIZE * i | prot;
idte_pmdp(va, &pmd_val(*pmd));
pmd_val(*pmd) = __pa(pte) | SEGMENT_ENTRY_TT_SEGMENT;
}
/*
* Splits a huge pud (region 3) DAT table entry into equivalent 1MB large
* pages.
* @pud The pud to split, it must be huge.
* @va the virtual address corresponding to this pud.
*/
static void split_pud(pud_t *pud, uintptr_t va)
{
phys_addr_t pa = pud_val(*pud) & REGION3_ENTRY_RFAA;
unsigned long i, prot;
pmd_t *pmd;
assert(pud_huge(*pud));
pmd = alloc_pages(SEGMENT_TABLE_ORDER);
prot = pud_val(*pud) & (REGION3_ENTRY_IEP | REGION_ENTRY_P);
for (i = 0; i < SEGMENT_TABLE_ENTRIES; i++)
pmd_val(pmd[i]) = pa | SZ_1M * i | prot | SEGMENT_ENTRY_FC | SEGMENT_ENTRY_TT_SEGMENT;
idte_pudp(va, &pud_val(*pud));
pud_val(*pud) = __pa(pmd) | REGION_ENTRY_TT_REGION3 | REGION_TABLE_LENGTH;
}
void *get_dat_entry(pgd_t *pgtable, void *vaddr, enum pgt_level level)
{
uintptr_t va = (uintptr_t)vaddr;
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
assert(level && (level <= 5));
pgd = pgd_offset(pgtable, va);
if (level == pgtable_level_pgd)
return pgd;
p4d = p4d_alloc(pgd, va);
if (level == pgtable_level_p4d)
return p4d;
pud = pud_alloc(p4d, va);
if (level == pgtable_level_pud)
return pud;
if (!pud_none(*pud) && pud_huge(*pud))
split_pud(pud, va);
pmd = get_pmd(pud, va);
if (level == pgtable_level_pmd)
return pmd;
if (!pmd_none(*pmd) && pmd_large(*pmd))
split_pmd(pmd, va);
return get_pte(pmd, va);
}
void *split_page(pgd_t *pgtable, void *vaddr, enum pgt_level level)
{
assert((level >= 3) && (level <= 5));
return get_dat_entry(pgtable ? pgtable : table_root, vaddr, level);
}
phys_addr_t virt_to_pte_phys(pgd_t *pgtable, void *vaddr)
{
uintptr_t va = (uintptr_t)vaddr;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
pud = get_pud(pgtable, va);
if (pud_huge(*pud))
return (pud_val(*pud) & REGION3_ENTRY_RFAA) | (va & ~REGION3_ENTRY_RFAA);
pmd = get_pmd(pud, va);
if (pmd_large(*pmd))
return (pmd_val(*pmd) & SEGMENT_ENTRY_SFAA) | (va & ~SEGMENT_ENTRY_SFAA);
pte = get_pte(pmd, va);
return (pte_val(*pte) & PAGE_MASK) | (va & ~PAGE_MASK);
}
/*
* Get the DAT table entry of the given level for the given address,
* splitting if necessary. If the entry was not invalid, invalidate it, and
* return the pointer to the entry and, if requested, its old value.
* @pgtable root of the page tables
* @vaddr virtual address
* @level 3 (for 2GB pud), 4 (for 1MB pmd) or 5 (for 4kB pages)
* @old if not NULL, will be written with the old value of the DAT table
* entry before invalidation
*/
static void *dat_get_and_invalidate(pgd_t *pgtable, void *vaddr, enum pgt_level level, unsigned long *old)
{
unsigned long va = (unsigned long)vaddr;
void *ptr;
ptr = get_dat_entry(pgtable, vaddr, level);
if (old)
*old = *(unsigned long *)ptr;
if ((level == pgtable_level_pgd) && !pgd_none(*(pgd_t *)ptr))
idte_pgdp(va, ptr);
else if ((level == pgtable_level_p4d) && !p4d_none(*(p4d_t *)ptr))
idte_p4dp(va, ptr);
else if ((level == pgtable_level_pud) && !pud_none(*(pud_t *)ptr))
idte_pudp(va, ptr);
else if ((level == pgtable_level_pmd) && !pmd_none(*(pmd_t *)ptr))
idte_pmdp(va, ptr);
else if (!pte_none(*(pte_t *)ptr))
ipte(va, ptr);
return ptr;
}
static void cleanup_pmd(pmd_t *pmd)
{
/* was invalid or large, nothing to do */
if (pmd_none(*pmd) || pmd_large(*pmd))
return;
/* was not large, free the corresponding page table */
free_pages((void *)(pmd_val(*pmd) & PAGE_MASK));
}
static void cleanup_pud(pud_t *pud)
{
unsigned long i;
pmd_t *pmd;
/* was invalid or large, nothing to do */
if (pud_none(*pud) || pud_huge(*pud))
return;
/* recursively clean up all pmds if needed */
pmd = (pmd_t *)(pud_val(*pud) & PAGE_MASK);
for (i = 0; i < SEGMENT_TABLE_ENTRIES; i++)
cleanup_pmd(pmd + i);
/* free the corresponding segment table */
free_pages(pmd);
}
/*
* Set the DAT entry for the given level of the given virtual address. If a
* mapping already existed, it is overwritten. If an existing mapping with
* smaller pages existed, all the lower tables are freed.
* Returns the pointer to the DAT table entry.
* @pgtable root of the page tables
* @val the new value for the DAT table entry
* @vaddr the virtual address
* @level 3 for pud (region 3), 4 for pmd (segment) and 5 for pte (pages)
*/
static void *set_dat_entry(pgd_t *pgtable, unsigned long val, void *vaddr, enum pgt_level level)
{
unsigned long old, *res;
res = dat_get_and_invalidate(pgtable, vaddr, level, &old);
if (level == pgtable_level_pmd)
cleanup_pmd((pmd_t *)&old);
if (level == pgtable_level_pud)
cleanup_pud((pud_t *)&old);
*res = val;
return res;
}
pteval_t *install_page(pgd_t *pgtable, phys_addr_t phys, void *vaddr)
{
assert(IS_ALIGNED(phys, PAGE_SIZE));
assert(IS_ALIGNED((uintptr_t)vaddr, PAGE_SIZE));
return set_dat_entry(pgtable, phys, vaddr, pgtable_level_pte);
}
pmdval_t *install_large_page(pgd_t *pgtable, phys_addr_t phys, void *vaddr)
{
assert(IS_ALIGNED(phys, SZ_1M));
assert(IS_ALIGNED((uintptr_t)vaddr, SZ_1M));
return set_dat_entry(pgtable, phys | SEGMENT_ENTRY_FC, vaddr, pgtable_level_pmd);
}
pudval_t *install_huge_page(pgd_t *pgtable, phys_addr_t phys, void *vaddr)
{
assert(IS_ALIGNED(phys, SZ_2G));
assert(IS_ALIGNED((uintptr_t)vaddr, SZ_2G));
return set_dat_entry(pgtable, phys | REGION3_ENTRY_FC | REGION_ENTRY_TT_REGION3, vaddr, pgtable_level_pud);
}
void protect_dat_entry(void *vaddr, unsigned long prot, enum pgt_level level)
{
unsigned long old, *ptr;
ptr = dat_get_and_invalidate(table_root, vaddr, level, &old);
*ptr = old | prot;
}
void unprotect_dat_entry(void *vaddr, unsigned long prot, enum pgt_level level)
{
unsigned long old, *ptr;
ptr = dat_get_and_invalidate(table_root, vaddr, level, &old);
*ptr = old & ~prot;
}
void protect_range(void *start, unsigned long len, unsigned long prot)
{
uintptr_t curr = (uintptr_t)start & PAGE_MASK;
len &= PAGE_MASK;
for (; len; len -= PAGE_SIZE, curr += PAGE_SIZE)
protect_dat_entry((void *)curr, prot, 5);
}
void unprotect_range(void *start, unsigned long len, unsigned long prot)
{
uintptr_t curr = (uintptr_t)start & PAGE_MASK;
len &= PAGE_MASK;
for (; len; len -= PAGE_SIZE, curr += PAGE_SIZE)
unprotect_dat_entry((void *)curr, prot, 5);
}
static void setup_identity(pgd_t *pgtable, phys_addr_t start_addr,
phys_addr_t end_addr)
{
phys_addr_t cur;
start_addr &= PAGE_MASK;
for (cur = start_addr; true; cur += PAGE_SIZE) {
if (start_addr < end_addr && cur >= end_addr)
break;
if (start_addr > end_addr && cur <= end_addr)
break;
install_page(pgtable, cur, __va(cur));
}
}
void *setup_mmu(phys_addr_t phys_end, void *unused)
{
pgd_t *page_root;
/* allocate a region-1 table */
page_root = pgd_alloc_one();
/* map all physical memory 1:1 */
setup_identity(page_root, 0, phys_end);
/* generate 128MB of invalid adresses at the end (for testing PGM) */
init_alloc_vpage((void *) -(1UL << 27));
setup_identity(page_root, -(1UL << 27), 0);
/* finally enable DAT with the new table */
mmu_enable(page_root);
table_root = page_root;
return page_root;
}