/*
 *  linux/arch/arm/mm/rodata.c
 *
 *  Copyright (C) 2011 Google, Inc.
 *
 *  Author: Colin Cross <ccross@android.com>
 *
 *  Based on x86 implementation in arch/x86/mm/init_32.c
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include <linux/kernel.h>
#include <linux/mm.h>
#include <linux/module.h>

#include <asm/cache.h>
#include <asm/pgtable.h>
#include <asm/rodata.h>
#include <asm/sections.h>
#include <asm/tlbflush.h>

#include "mm.h"

static int kernel_set_to_readonly __read_mostly;

#ifdef CONFIG_DEBUG_RODATA_TEST
static const int rodata_test_data = 0xC3;

static noinline void rodata_test(void)
{
	int result;

	pr_info("%s: attempting to write to read-only section:\n", __func__);

	if (*(volatile int *)&rodata_test_data != 0xC3) {
		pr_err("read only data changed before test\n");
		return;
	}

	/*
	 * Attempt to to write to rodata_test_data, trapping the expected
	 * data abort.  If the trap executed, result will be 1.  If it didn't,
	 * result will be 0xFF.
	 */
	asm volatile(
		"0:	str	%[zero], [%[rodata_test_data]]\n"
		"	mov	%[result], #0xFF\n"
		"	b	2f\n"
		"1:	mov	%[result], #1\n"
		"2:\n"

		/* Exception fixup - if store at label 0 faults, jumps to 1 */
		".pushsection __ex_table, \"a\"\n"
		"	.long	0b, 1b\n"
		".popsection\n"

		: [result] "=r" (result)
		: [rodata_test_data] "r" (&rodata_test_data), [zero] "r" (0)
		: "memory"
	);

	if (result == 1)
		pr_info("write to read-only section trapped, success\n");
	else
		pr_err("write to read-only section NOT trapped, test failed\n");

	if (*(volatile int *)&rodata_test_data != 0xC3)
		pr_err("read only data changed during write\n");
}
#else
static inline void rodata_test(void) { }
#endif

static int set_page_attributes(unsigned long virt, int numpages,
	pte_t (*f)(pte_t))
{
	pmd_t *pmd;
	pte_t *pte;
	unsigned long start = virt;
	unsigned long end = virt + (numpages << PAGE_SHIFT);
	unsigned long pmd_end;

	while (virt < end) {
		pmd = pmd_off_k(virt);
		pmd_end = min(ALIGN(virt + 1, PMD_SIZE), end);

		if ((pmd_val(*pmd) & PMD_TYPE_MASK) != PMD_TYPE_TABLE) {
			pr_err("%s: pmd %p=%08lx for %08lx not page table\n",
				__func__, pmd, pmd_val(*pmd), virt);
			virt = pmd_end;
			continue;
		}

		while (virt < pmd_end) {
			pte = pte_offset_kernel(pmd, virt);
			set_pte_ext(pte, f(*pte), 0);
			virt += PAGE_SIZE;
		}
	}

	flush_tlb_kernel_range(start, end);

	return 0;
}

int set_memory_ro(unsigned long virt, int numpages)
{
	return set_page_attributes(virt, numpages, pte_wrprotect);
}
EXPORT_SYMBOL(set_memory_ro);

int set_memory_rw(unsigned long virt, int numpages)
{
	return set_page_attributes(virt, numpages, pte_mkwrite);
}
EXPORT_SYMBOL(set_memory_rw);

void set_kernel_text_rw(void)
{
	unsigned long start = PAGE_ALIGN((unsigned long)_text);
	unsigned long size = PAGE_ALIGN((unsigned long)__end_rodata) - start;

	if (!kernel_set_to_readonly)
		return;

	pr_debug("Set kernel text: %lx - %lx to read-write\n",
		 start, start + size);

	set_memory_rw(start, size >> PAGE_SHIFT);
}

void set_kernel_text_ro(void)
{
	unsigned long start = PAGE_ALIGN((unsigned long)_text);
	unsigned long size = PAGE_ALIGN((unsigned long)__end_rodata) - start;

	if (!kernel_set_to_readonly)
		return;

	pr_info_once("Write protecting the kernel text section %lx - %lx\n",
		start, start + size);

	pr_debug("Set kernel text: %lx - %lx to read only\n",
		 start, start + size);

	set_memory_ro(start, size >> PAGE_SHIFT);
}

void mark_rodata_ro(void)
{
	kernel_set_to_readonly = 1;

	set_kernel_text_ro();

	rodata_test();
}