blob: 0876216ceee671af764dc8f0602f8f8ef8d6e861 [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0
/*
* MMU-generic set_memory implementation for powerpc
*
* Copyright 2019-2021, IBM Corporation.
*/
#include <linux/mm.h>
#include <linux/vmalloc.h>
#include <linux/set_memory.h>
#include <asm/mmu.h>
#include <asm/page.h>
#include <asm/pgtable.h>
/*
* Updates the attributes of a page in three steps:
*
* 1. invalidate the page table entry
* 2. flush the TLB
* 3. install the new entry with the updated attributes
*
* Invalidating the pte means there are situations where this will not work
* when in theory it should.
* For example:
* - removing write from page whilst it is being executed
* - setting a page read-only whilst it is being read by another CPU
*
*/
static int change_page_attr(pte_t *ptep, unsigned long addr, void *data)
{
long action = (long)data;
pte_t pte;
spin_lock(&init_mm.page_table_lock);
/* invalidate the PTE so it's safe to modify */
pte = ptep_get_and_clear(&init_mm, addr, ptep);
flush_tlb_kernel_range(addr, addr + PAGE_SIZE);
/* modify the PTE bits as desired, then apply */
switch (action) {
case SET_MEMORY_RO:
pte = pte_wrprotect(pte);
break;
case SET_MEMORY_RW:
pte = pte_mkwrite(pte_mkdirty(pte));
break;
case SET_MEMORY_NX:
pte = pte_exprotect(pte);
break;
case SET_MEMORY_X:
pte = pte_mkexec(pte);
break;
default:
WARN_ON_ONCE(1);
break;
}
set_pte_at(&init_mm, addr, ptep, pte);
/* See ptesync comment in radix__set_pte_at() */
if (radix_enabled())
asm volatile("ptesync": : :"memory");
spin_unlock(&init_mm.page_table_lock);
return 0;
}
int change_memory_attr(unsigned long addr, int numpages, long action)
{
unsigned long start = ALIGN_DOWN(addr, PAGE_SIZE);
unsigned long size = numpages * PAGE_SIZE;
if (!numpages)
return 0;
if (WARN_ON_ONCE(is_vmalloc_or_module_addr((void *)addr) &&
is_vm_area_hugepages((void *)addr)))
return -EINVAL;
#ifdef CONFIG_PPC_BOOK3S_64
/*
* On hash, the linear mapping is not in the Linux page table so
* apply_to_existing_page_range() will have no effect. If in the future
* the set_memory_* functions are used on the linear map this will need
* to be updated.
*/
if (!radix_enabled()) {
int region = get_region_id(addr);
if (WARN_ON_ONCE(region != VMALLOC_REGION_ID && region != IO_REGION_ID))
return -EINVAL;
}
#endif
return apply_to_existing_page_range(&init_mm, start, size,
change_page_attr, (void *)action);
}
/*
* Set the attributes of a page:
*
* This function is used by PPC32 at the end of init to set final kernel memory
* protection. It includes changing the maping of the page it is executing from
* and data pages it is using.
*/
static int set_page_attr(pte_t *ptep, unsigned long addr, void *data)
{
pgprot_t prot = __pgprot((unsigned long)data);
spin_lock(&init_mm.page_table_lock);
set_pte_at(&init_mm, addr, ptep, pte_modify(*ptep, prot));
flush_tlb_kernel_range(addr, addr + PAGE_SIZE);
spin_unlock(&init_mm.page_table_lock);
return 0;
}
int set_memory_attr(unsigned long addr, int numpages, pgprot_t prot)
{
unsigned long start = ALIGN_DOWN(addr, PAGE_SIZE);
unsigned long sz = numpages * PAGE_SIZE;
if (numpages <= 0)
return 0;
return apply_to_existing_page_range(&init_mm, start, sz, set_page_attr,
(void *)pgprot_val(prot));
}