/*
 * Basic implementation of a SW emulation of the domain manager feature in
 * ARM architecture.  Assumes single processor ARMv7 chipset.
 *
 * Requires hooks to be alerted to any runtime changes of dacr or MMU context.
 *
 * Copyright (c) 2009, Code Aurora Forum. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 and
 * only version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include <linux/sched.h>
#include <asm/domain.h>
#include <asm/pgtable.h>
#include <asm/tlbflush.h>
#include <linux/module.h>

#define DOMAIN_MANAGER_BITS (0xAAAAAAAA)

#define DFSR_DOMAIN(dfsr) ((dfsr >> 4) & (16-1))

#define FSR_PERMISSION_FAULT(fsr) ((fsr & 0x40D) == 0x00D)
#define FSR_PERMISSION_SECT(fsr) ((fsr & 0x40F) == 0x00D)

/* ARMv7 MMU HW Macros.  Not conveniently defined elsewhere */
#define MMU_TTB_ADDRESS(x)   ((u32 *)(((u32)(x)) & ~((1 << 14) - 1)))
#define MMU_PMD_INDEX(addr) (((u32)addr) >> SECTION_SHIFT)
#define MMU_TABLE_ADDRESS(x) ((u32 *)((x) & ~((1 << 10) - 1)))
#define MMU_TABLE_INDEX(x) ((((u32)x) >> 12) & (256 - 1))

/* Convenience Macros */
#define PMD_IS_VALID(x) (PMD_IS_TABLE(x) || PMD_IS_SECTION(x))
#define PMD_IS_TABLE(x) ((x & PMD_TYPE_MASK) == PMD_TYPE_TABLE)
#define PMD_IS_SECTION(x) ((x & PMD_TYPE_MASK) == PMD_TYPE_SECT)
#define PMD_IS_SUPERSECTION(x) \
	(PMD_IS_SECTION(x) && ((x & PMD_SECT_SUPER) == PMD_SECT_SUPER))

#define PMD_GET_DOMAIN(x)					\
	(PMD_IS_TABLE(x) ||					\
	(PMD_IS_SECTION(x) && !PMD_IS_SUPERSECTION(x)) ?	\
		 0 : (x >> 5) & (16-1))

#define PTE_IS_LARGE(x) ((x & PTE_TYPE_MASK) == PTE_TYPE_LARGE)


/* Only DOMAIN_MMU_ENTRIES will be granted access simultaneously */
#define DOMAIN_MMU_ENTRIES (8)

#define LRU_INC(lru) ((lru + 1) >= DOMAIN_MMU_ENTRIES ? 0 : lru + 1)


static DEFINE_SPINLOCK(edm_lock);

static u32 edm_manager_bits;

struct domain_entry_save {
	u32 *mmu_entry;
	u32 *addr;
	u32 value;
	u16 sect;
	u16 size;
};

static struct domain_entry_save edm_save[DOMAIN_MMU_ENTRIES];

static u32 edm_lru;


/*
 *  Return virtual address of pmd (level 1) entry for addr
 *
 *  This routine walks the ARMv7 page tables in HW.
 */
static inline u32 *__get_pmd_v7(u32 *addr)
{
	u32 *ttb;

	__asm__ __volatile__(
		"mrc	p15, 0, %0, c2, c0, 0	@ ttbr0\n\t"
		: "=r" (ttb)
		:
	);

	return __va(MMU_TTB_ADDRESS(ttb) + MMU_PMD_INDEX(addr));
}

/*
 *  Return virtual address of pte (level 2) entry for addr
 *
 *  This routine walks the ARMv7 page tables in HW.
 */
static inline u32 *__get_pte_v7(u32 *addr)
{
	u32 *pmd = __get_pmd_v7(addr);
	u32 *table_pa = pmd && PMD_IS_TABLE(*pmd) ?
		MMU_TABLE_ADDRESS(*pmd) : 0;
	u32 *entry = table_pa ? __va(table_pa[MMU_TABLE_INDEX(addr)]) : 0;

	return entry;
}

/*
 *  Invalidate the TLB for a given address for the current context
 *
 *  After manipulating access permissions, TLB invalidation changes are
 *  observed
 */
static inline void __tlb_invalidate(u32 *addr)
{
	__asm__ __volatile__(
		"mrc	p15, 0, %%r2, c13, c0, 1	@ contextidr\n\t"
		"and %%r2, %%r2, #0xff			@ asid\n\t"
		"mov %%r3, %0, lsr #12			@ mva[31:12]\n\t"
		"orr %%r2, %%r2, %%r3, lsl #12		@ tlb mva and asid\n\t"
		"mcr	p15, 0, %%r2, c8, c7, 1		@ utlbimva\n\t"
		"isb"
		:
		: "r" (addr)
		: "r2", "r3"
	);
}

/*
 *  Set HW MMU entry and do required synchronization operations.
 */
static inline void __set_entry(u32 *entry, u32 *addr, u32 value, int size)
{
	int i;

	if (!entry)
		return;

	entry = (u32 *)((u32) entry & ~(size * sizeof(u32) - 1));

	for (i = 0; i < size; i++)
		entry[i] = value;

	__asm__ __volatile__(
		"mcr	p15, 0, %0, c7, c10, 1		@ flush entry\n\t"
		"dsb\n\t"
		"isb\n\t"
		:
		: "r" (entry)
	);
	__tlb_invalidate(addr);
}

/*
 *  Return the number of duplicate entries associated with entry value.
 *  Supersections and Large page table entries are replicated 16x.
 */
static inline int __entry_size(int sect, int value)
{
	u32 size;

	if (sect)
		size = PMD_IS_SUPERSECTION(value) ? 16 : 1;
	else
		size = PTE_IS_LARGE(value) ? 16 : 1;

	return size;
}

/*
 *  Change entry permissions to emulate domain manager access
 */
static inline int __manager_perm(int sect, int value)
{
	u32 edm_value;

	if (sect) {
		edm_value = (value & ~(PMD_SECT_APX | PMD_SECT_XN)) |
		(PMD_SECT_AP_READ | PMD_SECT_AP_WRITE);
	} else {
		edm_value = (value & ~(PTE_EXT_APX | PTE_EXT_XN)) |
			(PTE_EXT_AP1 | PTE_EXT_AP0);
	}
	return edm_value;
}

/*
 *  Restore original HW MMU entry.  Cancels domain manager access
 */
static inline void __restore_entry(int index)
{
	struct domain_entry_save *entry = &edm_save[index];
	u32 edm_value;

	if (!entry->mmu_entry)
		return;

	edm_value = __manager_perm(entry->sect, entry->value);

	if (*entry->mmu_entry == edm_value)
		__set_entry(entry->mmu_entry, entry->addr,
			entry->value, entry->size);

	entry->mmu_entry = 0;
}

/*
 *  Modify HW MMU entry to grant domain manager access for a given MMU entry.
 *  This adds full read, write, and exec access permissions.
 */
static inline void __set_manager(int sect, u32 *addr)
{
	u32 *entry = sect ? __get_pmd_v7(addr) : __get_pte_v7(addr);
	u32 value;
	u32 edm_value;
	u16 size;

	if (!entry)
		return;

	value = *entry;

	size = __entry_size(sect, value);
	edm_value = __manager_perm(sect, value);

	__set_entry(entry, addr, edm_value, size);

	__restore_entry(edm_lru);

	edm_save[edm_lru].mmu_entry = entry;
	edm_save[edm_lru].addr = addr;
	edm_save[edm_lru].value = value;
	edm_save[edm_lru].sect = sect;
	edm_save[edm_lru].size = size;

	edm_lru = LRU_INC(edm_lru);
}

/*
 *  Restore original HW MMU entries.
 *
 *  entry - MVA for HW MMU entry
 */
static inline void __restore(void)
{
	if (unlikely(edm_manager_bits)) {
		u32 i;

		for (i = 0; i < DOMAIN_MMU_ENTRIES; i++)
			__restore_entry(i);
	}
}

/*
 * Common abort handler code
 *
 * If domain manager was actually set, permission fault would not happen.
 * Open access permissions to emulate.  Save original settings to restore
 * later. Return 1 to pretend fault did not happen.
 */
static int __emulate_domain_manager_abort(u32 fsr, u32 far, int dabort)
{
	if (unlikely(FSR_PERMISSION_FAULT(fsr) && edm_manager_bits)) {
		int domain = dabort ? DFSR_DOMAIN(fsr) : PMD_GET_DOMAIN(far);
		if (edm_manager_bits & domain_val(domain, DOMAIN_MANAGER)) {
			unsigned long flags;

			spin_lock_irqsave(&edm_lock, flags);

			__set_manager(FSR_PERMISSION_SECT(fsr), (u32 *) far);

			spin_unlock_irqrestore(&edm_lock, flags);
			return 1;
		}
	}
	return 0;
}

/*
 * Change domain setting.
 *
 * Lock and restore original contents.  Extract and save manager bits.  Set
 * DACR, excluding manager bits.
 */
void emulate_domain_manager_set(u32 domain)
{
	unsigned long flags;

	spin_lock_irqsave(&edm_lock, flags);

	if (edm_manager_bits != (domain & DOMAIN_MANAGER_BITS)) {
		__restore();
		edm_manager_bits = domain & DOMAIN_MANAGER_BITS;
	}

	__asm__ __volatile__(
		"mcr	p15, 0, %0, c3, c0, 0	@ set domain\n\t"
		"isb"
		:
		: "r" (domain & ~DOMAIN_MANAGER_BITS)
	);

	spin_unlock_irqrestore(&edm_lock, flags);
}
EXPORT_SYMBOL_GPL(emulate_domain_manager_set);

/*
 * Switch thread context.  Restore original contents.
 */
void emulate_domain_manager_switch_mm(unsigned long pgd_phys,
	struct mm_struct *mm,
	void (*switch_mm)(unsigned long pgd_phys, struct mm_struct *))
{
	unsigned long flags;

	spin_lock_irqsave(&edm_lock, flags);

	__restore();

	/* Call underlying kernel handler */
	switch_mm(pgd_phys, mm);

	spin_unlock_irqrestore(&edm_lock, flags);
}
EXPORT_SYMBOL_GPL(emulate_domain_manager_switch_mm);

/*
 * Kernel data_abort hook
 */
int emulate_domain_manager_data_abort(u32 dfsr, u32 dfar)
{
	return __emulate_domain_manager_abort(dfsr, dfar, 1);
}
EXPORT_SYMBOL_GPL(emulate_domain_manager_data_abort);

/*
 * Kernel prefetch_abort hook
 */
int emulate_domain_manager_prefetch_abort(u32 ifsr, u32 ifar)
{
	return __emulate_domain_manager_abort(ifsr, ifar, 0);
}
EXPORT_SYMBOL_GPL(emulate_domain_manager_prefetch_abort);

