blob: 9dce7da857096a08952da2137d5a3b4c30cfbe8c [file] [log] [blame]
/*
* MMU enable and page table manipulation functions
*
* Copyright (C) 2014, Red Hat Inc, Andrew Jones <drjones@redhat.com>
*
* This work is licensed under the terms of the GNU LGPL, version 2.
*/
#include <cpumask.h>
#include <memregions.h>
#include <asm/setup.h>
#include <asm/thread_info.h>
#include <asm/mmu.h>
#include <asm/setup.h>
#include <asm/page.h>
#include <asm/io.h>
#include "alloc_page.h"
#include "vmalloc.h"
#include <asm/pgtable-hwdef.h>
#include <asm/pgtable.h>
#include <linux/compiler.h>
pgd_t *mmu_idmap;
/* CPU 0 starts with disabled MMU */
static cpumask_t mmu_enabled_cpumask;
bool mmu_enabled(void)
{
/*
* mmu_enabled is called from places that are guarding the
* use of exclusive ops (which require the mmu to be enabled).
* That means we CANNOT call anything from here that may use a
* spinlock, atomic bitop, etc., otherwise we'll recurse.
* [cpumask_]test_bit is safe though.
*/
if (is_user()) {
int cpu = current_thread_info()->cpu;
return cpumask_test_cpu(cpu, &mmu_enabled_cpumask);
}
return __mmu_enabled();
}
void mmu_mark_enabled(int cpu)
{
cpumask_set_cpu(cpu, &mmu_enabled_cpumask);
}
void mmu_mark_disabled(int cpu)
{
cpumask_clear_cpu(cpu, &mmu_enabled_cpumask);
}
extern void asm_mmu_enable(phys_addr_t pgtable);
void mmu_enable(pgd_t *pgtable)
{
struct thread_info *info = current_thread_info();
asm_mmu_enable(__pa(pgtable));
info->pgtable = pgtable;
mmu_mark_enabled(info->cpu);
}
extern void asm_mmu_disable(void);
void mmu_disable(void)
{
unsigned long sp = current_stack_pointer;
int cpu = current_thread_info()->cpu;
assert_msg(__virt_to_phys(sp) == sp,
"Attempting to disable MMU with non-identity mapped stack");
mmu_mark_disabled(cpu);
asm_mmu_disable();
}
static pteval_t *get_pte(pgd_t *pgtable, uintptr_t vaddr)
{
pgd_t *pgd = pgd_offset(pgtable, vaddr);
pud_t *pud = pud_alloc(pgd, vaddr);
pmd_t *pmd = pmd_alloc(pud, vaddr);
pte_t *pte = pte_alloc(pmd, vaddr);
return &pte_val(*pte);
}
static pteval_t *install_pte(pgd_t *pgtable, uintptr_t vaddr, pteval_t pte)
{
pteval_t *p_pte = get_pte(pgtable, vaddr);
WRITE_ONCE(*p_pte, pte);
flush_tlb_page(vaddr);
return p_pte;
}
static pteval_t *install_page_prot(pgd_t *pgtable, phys_addr_t phys,
uintptr_t vaddr, pgprot_t prot)
{
pteval_t pte = phys;
pte |= PTE_TYPE_PAGE | PTE_AF | PTE_SHARED;
pte |= pgprot_val(prot);
return install_pte(pgtable, vaddr, pte);
}
pteval_t *install_page(pgd_t *pgtable, phys_addr_t phys, void *virt)
{
return install_page_prot(pgtable, phys, (uintptr_t)virt,
__pgprot(PTE_WBWA | PTE_USER));
}
/*
* NOTE: The Arm architecture might require the use of a
* break-before-make sequence before making changes to a PTE and
* certain conditions are met (see Arm ARM D5-2669 for AArch64 and
* B3-1378 for AArch32 for more details).
*/
pteval_t *follow_pte(pgd_t *pgtable, uintptr_t vaddr)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
pgd = pgd_offset(pgtable, vaddr);
if (!pgd_valid(*pgd))
return NULL;
pud = pud_offset(pgd, vaddr);
if (!pud_valid(*pud))
return NULL;
pmd = pmd_offset(pud, vaddr);
if (!pmd_valid(*pmd))
return NULL;
if (pmd_huge(*pmd))
return &pmd_val(*pmd);
pte = pte_offset(pmd, vaddr);
if (!pte_valid(*pte))
return NULL;
return &pte_val(*pte);
}
phys_addr_t virt_to_pte_phys(pgd_t *pgtable, void *virt)
{
phys_addr_t mask;
pteval_t *pteval;
pteval = follow_pte(pgtable, (uintptr_t)virt);
if (!pteval) {
install_page(pgtable, (phys_addr_t)(unsigned long)virt, virt);
return (phys_addr_t)(unsigned long)virt;
}
if (pmd_huge(__pmd(*pteval)))
mask = PMD_MASK;
else
mask = PAGE_MASK;
return (*pteval & PHYS_MASK & mask) |
((phys_addr_t)(unsigned long)virt & ~mask);
}
void mmu_set_range_ptes(pgd_t *pgtable, uintptr_t virt_offset,
phys_addr_t phys_start, phys_addr_t phys_end,
pgprot_t prot)
{
phys_addr_t paddr = phys_start & PAGE_MASK;
uintptr_t vaddr = virt_offset & PAGE_MASK;
uintptr_t virt_end = phys_end - paddr + vaddr;
for (; vaddr < virt_end; vaddr += PAGE_SIZE, paddr += PAGE_SIZE)
install_page_prot(pgtable, paddr, vaddr, prot);
}
void mmu_set_range_sect(pgd_t *pgtable, uintptr_t virt_offset,
phys_addr_t phys_start, phys_addr_t phys_end,
pgprot_t prot)
{
phys_addr_t paddr = phys_start & PMD_MASK;
uintptr_t vaddr = virt_offset & PMD_MASK;
uintptr_t virt_end = phys_end - paddr + vaddr;
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pmd_t entry;
for (; vaddr < virt_end; vaddr += PMD_SIZE, paddr += PMD_SIZE) {
pmd_val(entry) = paddr;
pmd_val(entry) |= PMD_TYPE_SECT | PMD_SECT_AF | PMD_SECT_S;
pmd_val(entry) |= pgprot_val(prot);
pgd = pgd_offset(pgtable, vaddr);
pud = pud_alloc(pgd, vaddr);
pmd = pmd_alloc(pud, vaddr);
WRITE_ONCE(*pmd, entry);
flush_tlb_page(vaddr);
}
}
void *setup_mmu(phys_addr_t phys_end, void *unused)
{
struct mem_region *r;
/* 3G-4G region is reserved for vmalloc, cap phys_end at 3G */
if (phys_end > (3ul << 30))
phys_end = 3ul << 30;
#ifdef __aarch64__
init_alloc_vpage((void*)(4ul << 30));
assert_msg(system_supports_granule(PAGE_SIZE),
"Unsupported translation granule %ld\n", PAGE_SIZE);
#endif
if (!mmu_idmap)
mmu_idmap = alloc_page();
for (r = mem_regions; r->end; ++r) {
if (r->flags & (MR_F_IO | MR_F_RESERVED)) {
continue;
} else if (r->flags & MR_F_CODE) {
/* armv8 requires code shared between EL1 and EL0 to be read-only */
mmu_set_range_ptes(mmu_idmap, r->start, r->start, r->end,
__pgprot(PTE_WBWA | PTE_USER | PTE_RDONLY));
} else {
mmu_set_range_ptes(mmu_idmap, r->start, r->start, r->end,
__pgprot(PTE_WBWA | PTE_USER));
}
}
mmu_enable(mmu_idmap);
return mmu_idmap;
}
void __iomem *__ioremap(phys_addr_t phys_addr, size_t size)
{
phys_addr_t paddr_aligned = phys_addr & PAGE_MASK;
phys_addr_t paddr_end = PAGE_ALIGN(phys_addr + size);
pgprot_t prot = __pgprot(PTE_UNCACHED | PTE_USER | PTE_UXN | PTE_PXN);
pgd_t *pgtable;
assert(sizeof(long) == 8 || !(phys_addr >> 32));
if (mmu_enabled()) {
pgtable = current_thread_info()->pgtable;
} else {
if (!mmu_idmap)
mmu_idmap = alloc_page();
pgtable = mmu_idmap;
}
mmu_set_range_ptes(pgtable, paddr_aligned, paddr_aligned,
paddr_end, prot);
return (void __iomem *)(unsigned long)phys_addr;
}
phys_addr_t __virt_to_phys(unsigned long addr)
{
if (mmu_enabled()) {
pgd_t *pgtable = current_thread_info()->pgtable;
return virt_to_pte_phys(pgtable, (void *)addr);
}
return addr;
}
unsigned long __phys_to_virt(phys_addr_t addr)
{
/*
* We don't guarantee that phys_to_virt(virt_to_phys(vaddr)) == vaddr, but
* the default page tables do identity map all physical addresses, which
* means phys_to_virt(virt_to_phys((void *)paddr)) == paddr.
*/
assert(!mmu_enabled() || __virt_to_phys(addr) == addr);
return addr;
}
void mmu_clear_user(pgd_t *pgtable, unsigned long vaddr)
{
pteval_t *p_pte = follow_pte(pgtable, vaddr);
if (p_pte) {
pteval_t entry = *p_pte & ~PTE_USER;
WRITE_ONCE(*p_pte, entry);
flush_tlb_page(vaddr);
}
}