/*
 * Copyright 2018 The Hafnium Authors.
 *
 * Use of this source code is governed by a BSD-style
 * license that can be found in the LICENSE file or at
 * https://opensource.org/licenses/BSD-3-Clause.
 */

#include "hf/mm.h"

#include <stdatomic.h>
#include <stdint.h>

#include "hf/arch/init.h"
#include "hf/arch/mm.h"

#include "hf/check.h"
#include "hf/dlog.h"
#include "hf/layout.h"
#include "hf/plat/console.h"
#include "hf/static_assert.h"
#include "hf/std.h"

/**
 * This file has functions for managing the level 1 and 2 page tables used by
 * Hafnium. There is a level 1 mapping used by Hafnium itself to access memory,
 * and then a level 2 mapping per VM. The design assumes that all page tables
 * contain only 1-1 mappings, aligned on the block boundaries.
 */

/*
 * For stage 2, the input is an intermediate physical addresses rather than a
 * virtual address so:
 */
static_assert(
	sizeof(ptable_addr_t) == sizeof(uintpaddr_t),
	"Currently, the same code manages the stage 1 and stage 2 page tables "
	"which only works if the virtual and intermediate physical addresses "
	"are the same size. It looks like that assumption might not be holding "
	"so we need to check that everything is going to be ok.");

static struct mm_ptable ptable;
static struct spinlock ptable_lock;

static bool mm_stage2_invalidate = false;

/**
 * After calling this function, modifications to stage-2 page tables will use
 * break-before-make and invalidate the TLB for the affected range.
 */
void mm_vm_enable_invalidation(void)
{
	mm_stage2_invalidate = true;
}

/**
 * Rounds an address down to a page boundary.
 */
static ptable_addr_t mm_round_down_to_page(ptable_addr_t addr)
{
	return align_down(addr, PAGE_SIZE);
}

/**
 * Rounds an address up to a page boundary.
 */
static ptable_addr_t mm_round_up_to_page(ptable_addr_t addr)
{
	return align_up(addr, PAGE_SIZE);
}

/**
 * Calculates the size of the address space represented by a page table entry at
 * the given level. See also Arm ARM, table D8-15
 * - `level == 4`: 256 TiB (1 << 48)
 * - `level == 3`: 512 GiB (1 << 39)
 * - `level == 2`:   1 GiB (1 << 30)
 * - `level == 1`:   2 MiB (1 << 21)
 * - `level == 0`:   4 KiB (1 << 12)
 */
static size_t mm_entry_size(mm_level_t level)
{
	assert(level <= 4);
	return UINT64_C(1) << (PAGE_BITS + level * PAGE_LEVEL_BITS);
}

/**
 * Get the start address of the range mapped by the next block of the given
 * level.
 */
static ptable_addr_t mm_start_of_next_block(ptable_addr_t addr,
					    mm_level_t level)
{
	assert(level <= 4);
	return align_up(addr + 1, mm_entry_size(level));
}

/**
 * For a given address, calculates the maximum (plus one) address that can be
 * represented by the same table at the given level.
 */
static ptable_addr_t mm_level_end(ptable_addr_t addr, mm_level_t level)
{
	size_t offset = PAGE_BITS + (level + 1) * PAGE_LEVEL_BITS;

	return ((addr >> offset) + 1) << offset;
}

/**
 * For a given address, calculates the index at which its entry is stored in a
 * table at the given level. See also Arm ARM, table D8-14
 * - `level == 4`: bits[51:48]
 * - `level == 3`: bits[47:39]
 * - `level == 2`: bits[38:30]
 * - `level == 1`: bits[29:21]
 * - `level == 0`: bits[20:12]
 */
static size_t mm_index(ptable_addr_t addr, mm_level_t level)
{
	ptable_addr_t v = addr >> (PAGE_BITS + level * PAGE_LEVEL_BITS);

	return v & ((UINT64_C(1) << PAGE_LEVEL_BITS) - 1);
}

/**
 * Allocates a new page table.
 */
static struct mm_page_table *mm_alloc_page_tables(size_t count,
						  struct mpool *ppool)
{
	if (count == 1) {
		return mpool_alloc(ppool);
	}

	return mpool_alloc_contiguous(ppool, count, count);
}

/**
 * Returns the root level in the page table given the flags.
 */
static mm_level_t mm_root_level(const struct mm_ptable *ptable)
{
	return ptable->stage1 ? arch_mm_stage1_root_level()
			      : arch_mm_stage2_root_level();
}

/**
 * Returns the number of root-level tables given the flags.
 */
static uint8_t mm_root_table_count(const struct mm_ptable *ptable)
{
	return ptable->stage1 ? arch_mm_stage1_root_table_count()
			      : arch_mm_stage2_root_table_count();
}

/**
 * Invalidates the TLB for the given address range.
 */
static void mm_invalidate_tlb(const struct mm_ptable *ptable,
			      ptable_addr_t begin, ptable_addr_t end,
			      bool non_secure)
{
	if (ptable->stage1) {
		arch_mm_invalidate_stage1_range(ptable->id, va_init(begin),
						va_init(end));
	} else {
		arch_mm_invalidate_stage2_range(ptable->id, ipa_init(begin),
						ipa_init(end), non_secure);
	}
}

/**
 * Frees all page-table-related memory associated with the given pte at the
 * given level, including any subtables recursively.
 */
// NOLINTNEXTLINE(misc-no-recursion)
static void mm_free_page_pte(pte_t pte, mm_level_t level, struct mpool *ppool)
{
	struct mm_page_table *table;

	if (!arch_mm_pte_is_table(pte, level)) {
		return;
	}

	/* Recursively free any subtables. */
	table = arch_mm_table_from_pte(pte, level);
	for (size_t i = 0; i < MM_PTE_PER_PAGE; ++i) {
		mm_free_page_pte(table->entries[i], level - 1, ppool);
	}

	/* Free the table itself. */
	mpool_free(ppool, table);
}

/**
 * Returns the first address which cannot be encoded in page tables given by
 * `flags`. It is the exclusive end of the address space created by the tables.
 */
ptable_addr_t mm_ptable_addr_space_end(const struct mm_ptable *ptable)
{
	return mm_root_table_count(ptable) *
	       mm_entry_size(mm_root_level(ptable));
}

/**
 * Initialises the given page table.
 */
bool mm_ptable_init(struct mm_ptable *ptable, mm_asid_t id, bool stage1,
		    struct mpool *ppool)
{
	struct mm_page_table *root_tables;
	uint8_t root_table_count = stage1 ? arch_mm_stage1_root_table_count()
					  : arch_mm_stage2_root_table_count();
	mm_level_t root_level = stage1 ? arch_mm_stage1_root_level()
				       : arch_mm_stage2_root_level();

	root_tables = mm_alloc_page_tables(root_table_count, ppool);
	if (root_tables == NULL) {
		return false;
	}

	for (size_t i = 0; i < root_table_count; i++) {
		for (size_t j = 0; j < MM_PTE_PER_PAGE; j++) {
			root_tables[i].entries[j] =
				arch_mm_absent_pte(root_level - 1);
		}
	}

	/*
	 * TODO: halloc could return a virtual or physical address if mm not
	 * enabled?
	 */
	ptable->id = id;
	ptable->root_tables = root_tables;
	ptable->stage1 = stage1;
	return true;
}

/**
 * Frees all memory associated with the give page table.
 */
static void mm_ptable_fini(const struct mm_ptable *ptable, struct mpool *ppool)
{
	struct mm_page_table *root_tables = ptable->root_tables;
	mm_level_t root_level = mm_root_level(ptable);
	uint8_t root_table_count = mm_root_table_count(ptable);

	for (size_t i = 0; i < root_table_count; ++i) {
		for (size_t j = 0; j < MM_PTE_PER_PAGE; ++j) {
			mm_free_page_pte(root_tables[i].entries[j],
					 root_level - 1, ppool);
		}
	}

	mpool_add_chunk(ppool, root_tables,
			sizeof(struct mm_page_table) * root_table_count);
}

/**
 * Replaces a page table entry with the given value. If both old and new values
 * are valid, it performs a break-before-make sequence where it first writes an
 * invalid value to the PTE, flushes the TLB, then writes the actual new value.
 * This is to prevent cases where CPUs have different 'valid' values in their
 * TLBs, which may result in issues for example in cache coherency.
 */
static void mm_replace_entry(const struct mm_ptable *ptable,
			     ptable_addr_t begin, pte_t *pte, pte_t new_pte,
			     mm_level_t level, bool non_secure,
			     struct mpool *ppool)
{
	pte_t v = *pte;

	/*
	 * We need to do the break-before-make sequence if both values are
	 * present and the TLB is being invalidated.
	 */
	if ((ptable->stage1 || mm_stage2_invalidate) &&
	    arch_mm_pte_is_valid(v, level)) {
		*pte = arch_mm_absent_pte(level);
		mm_invalidate_tlb(ptable, begin, begin + mm_entry_size(level),
				  non_secure);
	}

	/* Assign the new pte. */
	*pte = new_pte;

	/* Free pages that aren't in use anymore. */
	mm_free_page_pte(v, level, ppool);
}

/**
 * Populates the provided page table entry with a reference to another table if
 * needed, that is, if it does not yet point to another table.
 *
 * Returns a pointer to the table the entry now points to.
 */
static struct mm_page_table *mm_populate_table_pte(struct mm_ptable *ptable,
						   ptable_addr_t begin,
						   pte_t *pte, mm_level_t level,
						   bool non_secure,
						   struct mpool *ppool)
{
	struct mm_page_table *ntable;
	pte_t v = *pte;
	pte_t new_pte;
	size_t inc;
	mm_level_t level_below = level - 1;

	/* Just return pointer to table if it's already populated. */
	if (arch_mm_pte_is_table(v, level)) {
		return arch_mm_table_from_pte(v, level);
	}

	/* Allocate a new table. */
	ntable = mm_alloc_page_tables(1, ppool);
	if (ntable == NULL) {
		dlog_error("Failed to allocate memory for page table\n");
		return NULL;
	}

	/* Determine template for new pte and its increment. */
	if (arch_mm_pte_is_block(v, level)) {
		inc = mm_entry_size(level_below);
		new_pte = arch_mm_block_pte(level_below,
					    arch_mm_block_from_pte(v, level),
					    arch_mm_pte_attrs(v, level));
	} else {
		inc = 0;
		new_pte = arch_mm_absent_pte(level_below);
	}

	/* Initialise entries in the new table. */
	for (size_t i = 0; i < MM_PTE_PER_PAGE; i++) {
		ntable->entries[i] = new_pte;
		new_pte += inc;
	}

	/* Ensure initialisation is visible before updating the pte. */
	atomic_thread_fence(memory_order_release);

	/* Replace the pte entry, doing a break-before-make if needed. */
	mm_replace_entry(ptable, begin, pte,
			 arch_mm_table_pte(level, pa_init((uintpaddr_t)ntable)),
			 level, non_secure, ppool);

	return ntable;
}

/**
 * Updates the page table at the given level to map the given address range to a
 * physical range using the provided (architecture-specific) attributes. Or if
 * `flags.unmap` is set, unmap the given range instead.
 *
 * This function calls itself recursively if it needs to update additional
 * levels, but the recursion is bound by the maximum number of levels in a page
 * table.
 */
// NOLINTNEXTLINE(misc-no-recursion)
static bool mm_map_level(struct mm_ptable *ptable, ptable_addr_t begin,
			 ptable_addr_t end, mm_attr_t attrs,
			 struct mm_page_table *child_table, mm_level_t level,
			 struct mm_flags flags, struct mpool *ppool)
{
	pte_t *pte = &child_table->entries[mm_index(begin, level)];
	ptable_addr_t level_end = mm_level_end(begin, level);
	size_t entry_size = mm_entry_size(level);
	bool commit = flags.commit;
	bool unmap = flags.unmap;
	bool non_secure = ((attrs & (1ULL << 57)) != 0);

	/* Cap end so that we don't go over the current level max. */
	if (end > level_end) {
		end = level_end;
	}

	/* Fill each entry in the table. */
	while (begin < end) {
		if (unmap ? !arch_mm_pte_is_present(*pte, level)
			  : arch_mm_pte_is_block(*pte, level) &&
				    arch_mm_pte_attrs(*pte, level) == attrs) {
			/*
			 * If the entry is already mapped with the right
			 * attributes, or already absent in the case of
			 * unmapping, no need to do anything; carry on to the
			 * next entry.
			 */
		} else if ((end - begin) >= entry_size &&
			   (unmap || arch_mm_is_block_allowed(level)) &&
			   is_aligned(begin, entry_size)) {
			/*
			 * If the entire entry is within the region we want to
			 * map, map/unmap the whole entry.
			 */
			if (commit) {
				pte_t new_pte =
					unmap ? arch_mm_absent_pte(level)
					      : arch_mm_block_pte(
							level, pa_init(begin),
							attrs);
				mm_replace_entry(ptable, begin, pte, new_pte,
						 level, non_secure, ppool);
			}
		} else {
			/*
			 * If the entry is already a subtable get it; otherwise
			 * replace it with an equivalent subtable and get that.
			 */
			struct mm_page_table *nt = mm_populate_table_pte(
				ptable, begin, pte, level, non_secure, ppool);
			if (nt == NULL) {
				return false;
			}

			/*
			 * Recurse to map/unmap the appropriate entries within
			 * the subtable.
			 */
			if (!mm_map_level(ptable, begin, end, attrs, nt,
					  level - 1, flags, ppool)) {
				return false;
			}
		}

		begin = mm_start_of_next_block(begin, level);
		pte++;
	}

	return true;
}

/**
 * Updates the page table from the root to map the given address range to a
 * physical range using the provided (architecture-specific) attributes.
 *
 * Flags:
 * - `flags.unmap`: unmap the given range instead of mapping it.
 * - `flags.commit`: the change is only committed if this flag is set.
 */
static bool mm_ptable_identity_map(struct mm_ptable *ptable, paddr_t pa_begin,
				   paddr_t pa_end, mm_attr_t attrs,
				   struct mm_flags flags, struct mpool *ppool)
{
	mm_level_t root_level = mm_root_level(ptable);
	ptable_addr_t ptable_end = mm_ptable_addr_space_end(ptable);
	ptable_addr_t end = mm_round_up_to_page(pa_addr(pa_end));
	ptable_addr_t begin = mm_round_down_to_page(pa_addr(pa_begin));
	struct mm_page_table *root_table =
		&ptable->root_tables[mm_index(begin, root_level)];

	/*
	 * Assert condition to communicate the API constraint of
	 * mm_root_level(), that isn't encoded in the types, to the static
	 * analyzer.
	 */
	assert(root_level >= 3);

	/* Cap end to stay within the bounds of the page table. */
	if (end > ptable_end) {
		dlog_verbose(
			"ptable_map: input range end falls outside of ptable "
			"address space (%#016lx > %#016lx), capping to ptable "
			"address space end\n",
			end, ptable_end);
		end = ptable_end;
	}

	if (begin >= end) {
		dlog_verbose(
			"ptable_map: input range is backwards (%#016lx >= "
			"%#016lx), request will have no effect\n",
			begin, end);
	} else if (pa_addr(pa_begin) >= pa_addr(pa_end)) {
		dlog_verbose(
			"ptable_map: input range was backwards (%#016lx >= "
			"%#016lx), but due to rounding the range %#016lx to "
			"%#016lx will be mapped\n",
			begin, end, pa_addr(pa_begin), pa_addr(pa_end));
	}

	while (begin < end) {
		if (!mm_map_level(ptable, begin, end, attrs, root_table,
				  root_level - 1, flags, ppool)) {
			return false;
		}
		begin = mm_start_of_next_block(begin, root_level);
		root_table++;
	}

	/*
	 * All TLB invalidations must be complete already if any entries were
	 * replaced by mm_replace_entry. Sync all page table writes so that code
	 * following this can use them.
	 */
	arch_mm_sync_table_writes();

	return true;
}

/*
 * Prepares the given page table for the given address mapping such that it
 * will be able to commit the change without failure. It does so by ensuring
 * the smallest granularity needed is available. This remains valid provided
 * subsequent operations do not decrease the granularity.
 *
 * In particular, multiple calls to this function will result in the
 * corresponding calls to commit the changes to succeed.
 */
static bool mm_ptable_identity_prepare(struct mm_ptable *ptable,
				       paddr_t pa_begin, paddr_t pa_end,
				       mm_attr_t attrs, struct mm_flags flags,
				       struct mpool *ppool)
{
	flags.commit = false;
	return mm_ptable_identity_map(ptable, pa_begin, pa_end, attrs, flags,
				      ppool);
}

/**
 * Commits the given address mapping to the page table assuming the operation
 * cannot fail. `mm_ptable_identity_prepare` must used correctly before this to
 * ensure this condition.
 *
 * Without the table being properly prepared, the commit may only partially
 * complete if it runs out of memory resulting in an inconsistent state that
 * isn't handled.
 *
 * Since the non-failure assumtion is used in the reasoning about the atomicity
 * of higher level memory operations, any detected violations result in a panic.
 *
 * TODO: remove ppool argument to be sure no changes are made.
 */
static void mm_ptable_identity_commit(struct mm_ptable *ptable,
				      paddr_t pa_begin, paddr_t pa_end,
				      mm_attr_t attrs, struct mm_flags flags,
				      struct mpool *ppool)
{
	flags.commit = true;
	CHECK(mm_ptable_identity_map(ptable, pa_begin, pa_end, attrs, flags,
				     ppool));
}

/**
 * Updates the given table such that the given physical address range is mapped
 * or not mapped into the address space with the architecture-agnostic mode
 * provided.
 *
 * The page table is updated using the separate prepare and commit stages so
 * that, on failure, a partial update of the address space cannot happen. The
 * table may be left with extra internal tables but the address space is
 * unchanged.
 */
static bool mm_ptable_identity_update(struct mm_ptable *ptable,
				      paddr_t pa_begin, paddr_t pa_end,
				      mm_attr_t attrs, struct mm_flags flags,
				      struct mpool *ppool)
{
	if (!mm_ptable_identity_prepare(ptable, pa_begin, pa_end, attrs, flags,
					ppool)) {
		return false;
	}

	mm_ptable_identity_commit(ptable, pa_begin, pa_end, attrs, flags,
				  ppool);

	return true;
}

static void mm_dump_entries(const pte_t *entries, mm_level_t level,
			    uint32_t indent);

static void mm_dump_block_entry(pte_t entry, mm_level_t level, uint32_t indent)
{
	mm_attr_t attrs = arch_mm_pte_attrs(entry, level);
	paddr_t addr = arch_mm_block_from_pte(entry, level);

	if (arch_mm_pte_is_valid(entry, level)) {
		if (level == 0) {
			dlog("page {\n");
		} else {
			dlog("block {\n");
		}
	} else {
		dlog("invalid_block {\n");
	}

	indent += 1;
	{
		dlog_indent(indent, ".addr  = %#016lx\n", pa_addr(addr));
		dlog_indent(indent, ".attrs = %#016lx\n", attrs);
	}
	indent -= 1;
	dlog_indent(indent, "}");
}

// NOLINTNEXTLINE(misc-no-recursion)
static void mm_dump_table_entry(pte_t entry, mm_level_t level, uint32_t indent)
{
	dlog("table {\n");
	indent += 1;
	{
		mm_attr_t attrs = arch_mm_pte_attrs(entry, level);
		const struct mm_page_table *child_table =
			arch_mm_table_from_pte(entry, level);
		paddr_t addr = pa_init((uintpaddr_t)child_table);

		dlog_indent(indent, ".pte   = %#016lx,\n", entry);
		dlog_indent(indent, ".attrs = %#016lx,\n", attrs);
		dlog_indent(indent, ".addr  = %#016lx,\n", pa_addr(addr));
		dlog_indent(indent, ".entries = ");
		mm_dump_entries(child_table->entries, level - 1, indent);
		dlog(",\n");
	}
	indent -= 1;
	dlog_indent(indent, "}");
}

// NOLINTNEXTLINE(misc-no-recursion)
static void mm_dump_entry(pte_t entry, mm_level_t level, uint32_t indent)
{
	switch (arch_mm_pte_type(entry, level)) {
	case PTE_TYPE_ABSENT:
		dlog("absent {}");
		break;
	case PTE_TYPE_INVALID_BLOCK:
	case PTE_TYPE_VALID_BLOCK: {
		mm_dump_block_entry(entry, level, indent);
		break;
	}
	case PTE_TYPE_TABLE: {
		mm_dump_table_entry(entry, level, indent);
		break;
	}
	}
}

// NOLINTNEXTLINE(misc-no-recursion)
static void mm_dump_entries(const pte_t *entries, mm_level_t level,
			    uint32_t indent)
{
	dlog("{\n");
	indent += 1;

	for (size_t i = 0; i < MM_PTE_PER_PAGE; i++) {
		pte_t entry = entries[i];

		if (arch_mm_pte_is_absent(entry, level)) {
			continue;
		}

		dlog_indent(indent, "[level = %u, index = %zu] = ", level, i);
		mm_dump_entry(entry, level, indent);
		dlog(",\n");
	}

	indent -= 1;
	dlog_indent(indent, "}");
}

/**
 * Writes the given table to the debug log.
 */
static void mm_ptable_dump(const struct mm_ptable *ptable)
{
	struct mm_page_table *root_tables = ptable->root_tables;
	mm_level_t root_level = mm_root_level(ptable);
	uint8_t root_table_count = mm_root_table_count(ptable);
	uint32_t indent = 0;

	dlog_indent(indent, "mm_ptable {\n");
	indent += 1;
	{
		dlog_indent(indent, ".stage = %s,\n",
			    ptable->stage1 ? "stage1" : "stage2");
		dlog_indent(indent, ".id = %hu,\n", ptable->id);
		dlog_indent(indent, ".root_tables = {\n");

		indent += 1;
		{
			for (size_t i = 0; i < root_table_count; ++i) {
				dlog_indent(
					indent,
					"[level = %u, index = %zu].entries = ",
					root_level, i);
				mm_dump_entries(root_tables[i].entries,
						root_level - 1, indent);
				dlog(",\n");
			}
		}
		indent -= 1;
		dlog_indent(indent, "},\n");
	}
	indent -= 1;
	dlog_indent(indent, "}\n");
}

/**
 * Given the table PTE entries all have identical attributes, returns the single
 * entry with which it can be replaced.
 */
static pte_t mm_merge_table_pte(pte_t table_pte, mm_level_t level)
{
	struct mm_page_table *table;
	mm_attr_t block_attrs;
	mm_attr_t table_attrs;
	mm_attr_t combined_attrs;
	paddr_t block_address;

	table = arch_mm_table_from_pte(table_pte, level);

	if (!arch_mm_pte_is_present(table->entries[0], level - 1)) {
		return arch_mm_absent_pte(level);
	}

	/* Might not be possible to merge the table into a single block. */
	if (!arch_mm_is_block_allowed(level)) {
		return table_pte;
	}

	/* Replace table with a single block, with equivalent attributes. */
	block_attrs = arch_mm_pte_attrs(table->entries[0], level - 1);
	table_attrs = arch_mm_pte_attrs(table_pte, level);
	combined_attrs =
		arch_mm_combine_table_entry_attrs(table_attrs, block_attrs);
	block_address = arch_mm_block_from_pte(table->entries[0], level - 1);

	return arch_mm_block_pte(level, block_address, combined_attrs);
}

/**
 * Defragments the given PTE by recursively replacing any tables with blocks or
 * absent entries where possible.
 */
// NOLINTNEXTLINE(misc-no-recursion)
static void mm_ptable_defrag_entry(struct mm_ptable *ptable,
				   ptable_addr_t base_addr, pte_t *entry,
				   mm_level_t level, bool non_secure,
				   struct mpool *ppool)
{
	struct mm_page_table *child_table;
	bool mergeable;
	bool base_present;
	mm_attr_t base_attrs;
	pte_t new_entry;

	if (!arch_mm_pte_is_table(*entry, level)) {
		return;
	}

	child_table = arch_mm_table_from_pte(*entry, level);

	/* Defrag the first entry in the table and use it as the base entry. */
	static_assert(MM_PTE_PER_PAGE >= 1, "There must be at least one PTE.");

	mm_ptable_defrag_entry(ptable, base_addr, &(child_table->entries[0]),
			       level - 1, non_secure, ppool);

	base_present =
		arch_mm_pte_is_present(child_table->entries[0], level - 1);
	base_attrs = arch_mm_pte_attrs(child_table->entries[0], level - 1);

	/*
	 * Defrag the remaining entries in the table and check whether they are
	 * compatible with the base entry meaning the table can be merged into a
	 * block entry. It assumes addresses are contiguous due to identity
	 * mapping.
	 */
	mergeable = true;
	for (size_t i = 1; i < MM_PTE_PER_PAGE; ++i) {
		bool present;
		ptable_addr_t block_addr =
			base_addr + (i * mm_entry_size(level - 1));

		mm_ptable_defrag_entry(ptable, block_addr,
				       &(child_table->entries[i]), level - 1,
				       non_secure, ppool);

		present = arch_mm_pte_is_present(child_table->entries[i],
						 level - 1);

		if (present != base_present) {
			mergeable = false;
			continue;
		}

		if (!present) {
			continue;
		}

		if (!arch_mm_pte_is_block(child_table->entries[i], level - 1)) {
			mergeable = false;
			continue;
		}

		if (arch_mm_pte_attrs(child_table->entries[i], level - 1) !=
		    base_attrs) {
			mergeable = false;
			continue;
		}
	}

	if (!mergeable) {
		return;
	}

	new_entry = mm_merge_table_pte(*entry, level);
	if (*entry != new_entry) {
		mm_replace_entry(ptable, base_addr, entry, (uintptr_t)new_entry,
				 level, non_secure, ppool);
	}
}

/**
 * Defragments the given page table by converting page table references to
 * blocks whenever possible.
 */
static void mm_ptable_defrag(struct mm_ptable *ptable, bool non_secure,
			     struct mpool *ppool)
{
	struct mm_page_table *root_tables = ptable->root_tables;
	mm_level_t root_level = mm_root_level(ptable);
	uint8_t root_table_count = mm_root_table_count(ptable);
	ptable_addr_t block_addr = 0;

	/*
	 * Loop through each entry in the table. If it points to another table,
	 * check if that table can be replaced by a block or an absent entry.
	 */
	for (size_t i = 0; i < root_table_count; ++i) {
		for (size_t j = 0; j < MM_PTE_PER_PAGE; ++j) {
			mm_ptable_defrag_entry(
				ptable, block_addr, &root_tables[i].entries[j],
				root_level - 1, non_secure, ppool);
			block_addr = mm_start_of_next_block(block_addr,
							    root_level - 1);
		}
	}

	arch_mm_sync_table_writes();
}

struct mm_get_attrs_state {
	/**
	 * The attributes the range is mapped with.
	 * Only valid if `got_attrs` is true.
	 */
	mm_attr_t attrs;
	/**
	 * The address of the first page that does not match the attributes of
	 * the pages before it in the range.
	 * Only valid if `got_mismatch` is true.
	 */
	ptable_addr_t mismatch;
	bool got_attrs : 1;
	bool got_mismatch : 1;
};

/**
 * Gets the attributes applied to the given range of stage-2 addresses at the
 * given level.
 *
 * The `got_attrs` argument is initially passed as false until `attrs` contains
 * attributes of the memory region at which point it is passed as true.
 *
 * The value returned in `attrs` is only valid if the function returns true.
 *
 * Returns true if the whole range has the same attributes and false otherwise.
 */
// NOLINTNEXTLINE(misc-no-recursion)
static struct mm_get_attrs_state mm_ptable_get_attrs_level(
	const struct mm_page_table *table, ptable_addr_t begin,
	ptable_addr_t end, mm_level_t level, struct mm_get_attrs_state state)
{
	const pte_t *pte = &table->entries[mm_index(begin, level)];
	ptable_addr_t level_end = mm_level_end(begin, level);

	/* Cap end so that we don't go over the current level max. */
	if (end > level_end) {
		end = level_end;
	}

	/* Check that each entry is owned. */
	while (begin < end && !state.got_mismatch) {
		switch (arch_mm_pte_type(*pte, level)) {
		case PTE_TYPE_TABLE: {
			const struct mm_page_table *child_table =
				arch_mm_table_from_pte(*pte, level);
			state = mm_ptable_get_attrs_level(
				child_table, begin, end, level - 1, state);
			break;
		}

		case PTE_TYPE_ABSENT:
		case PTE_TYPE_INVALID_BLOCK:
		case PTE_TYPE_VALID_BLOCK: {
			mm_attr_t block_attrs = arch_mm_pte_attrs(*pte, level);

			if (state.got_attrs && block_attrs != state.attrs) {
				state.mismatch = begin;
				state.got_mismatch = true;
				continue;
			}

			state.got_attrs = true;
			state.attrs = block_attrs;
			break;
		}
		}

		begin = mm_start_of_next_block(begin, level);
		pte++;
	}

	/* The entry is a valid block. */
	return state;
}

/**
 * Gets the attributes applied to the given range of addresses in the page
 * tables.
 *
 * The value returned in `attrs` is only valid if the function returns true.
 *
 * Returns true if the whole range has the same attributes and false otherwise.
 */
static struct mm_get_attrs_state mm_get_attrs(const struct mm_ptable *ptable,
					      ptable_addr_t begin,
					      ptable_addr_t end)
{
	mm_level_t root_level = mm_root_level(ptable);
	ptable_addr_t ptable_end = mm_ptable_addr_space_end(ptable);
	struct mm_page_table *root_table;
	struct mm_get_attrs_state state = {0};

	if (begin >= end) {
		dlog_verbose(
			"mm_get: input range is backwards (%#016lx >= "
			"%#016lx)\n",
			begin, end);
	}

	begin = mm_round_down_to_page(begin);
	end = mm_round_up_to_page(end);

	/* Fail if the addresses are out of range. */
	if (end > ptable_end) {
		return state;
	}

	root_table = &ptable->root_tables[mm_index(begin, root_level)];
	while (begin < end && !state.got_mismatch) {
		state = mm_ptable_get_attrs_level(root_table, begin, end,
						  root_level - 1, state);

		begin = mm_start_of_next_block(begin, root_level);
		root_table++;
	}

	return state;
}

bool mm_vm_init(struct mm_ptable *ptable, mm_asid_t id, struct mpool *ppool)
{
	return mm_ptable_init(ptable, id, false, ppool);
}

void mm_vm_fini(const struct mm_ptable *ptable, struct mpool *ppool)
{
	mm_ptable_fini(ptable, ppool);
}

/**
 * Selects flags to pass to the page table manipulation operation based on the
 * mapping mode.
 */
static struct mm_flags mm_mode_to_flags(mm_mode_t mode)
{
	struct mm_flags flags = {0};

	if ((mode & MM_MODE_UNMAPPED_MASK) == MM_MODE_UNMAPPED_MASK) {
		flags.unmap = true;
	}

	return flags;
}

/**
 * See `mm_ptable_identity_prepare`.
 *
 * This must be called before `mm_identity_commit` for the same mapping.
 *
 * Returns true on success, or false if the update would fail.
 */
bool mm_identity_prepare(struct mm_ptable *ptable, paddr_t begin, paddr_t end,
			 mm_mode_t mode, struct mpool *ppool)
{
	struct mm_flags flags = mm_mode_to_flags(mode);

	assert(ptable->stage1);
	return mm_ptable_identity_prepare(ptable, begin, end,
					  arch_mm_mode_to_stage1_attrs(mode),
					  flags, ppool);
}

/**
 * See `mm_ptable_identity_commit`.
 *
 * `mm_identity_prepare` must be called before this for the same mapping.
 */
void *mm_identity_commit(struct mm_ptable *ptable, paddr_t begin, paddr_t end,
			 mm_mode_t mode, struct mpool *ppool)
{
	struct mm_flags flags = mm_mode_to_flags(mode);

	assert(ptable->stage1);
	mm_ptable_identity_commit(ptable, begin, end,
				  arch_mm_mode_to_stage1_attrs(mode), flags,
				  ppool);
	return ptr_from_va(va_from_pa(begin));
}

/**
 * See `mm_ptable_identity_prepare`.
 *
 * This must be called before `mm_vm_identity_commit` for the same mapping.
 *
 * Returns true on success, or false if the update would fail.
 */
bool mm_vm_identity_prepare(struct mm_ptable *ptable, paddr_t begin,
			    paddr_t end, mm_mode_t mode, struct mpool *ppool)
{
	struct mm_flags flags = mm_mode_to_flags(mode);

	return mm_ptable_identity_prepare(ptable, begin, end,
					  arch_mm_mode_to_stage2_attrs(mode),
					  flags, ppool);
}

/**
 * See `mm_ptable_identity_commit`.
 *
 * `mm_vm_identity_prepare` must be called before this for the same mapping.
 */
void mm_vm_identity_commit(struct mm_ptable *ptable, paddr_t begin, paddr_t end,
			   mm_mode_t mode, struct mpool *ppool, ipaddr_t *ipa)
{
	struct mm_flags flags = mm_mode_to_flags(mode);

	mm_ptable_identity_commit(ptable, begin, end,
				  arch_mm_mode_to_stage2_attrs(mode), flags,
				  ppool);

	if (ipa != NULL) {
		*ipa = ipa_from_pa(begin);
	}
}

/**
 * Updates a VM's page table such that the given physical address range is
 * mapped in the address space at the corresponding address range in the
 * architecture-agnostic mode provided.
 *
 * mm_vm_defrag should always be called after a series of page table updates,
 * whether they succeed or fail. This is because on failure extra page table
 * entries may have been allocated and then not used, while on success it may be
 * possible to compact the page table by merging several entries into a block.
 *
 * Returns true on success, or false if the update failed and no changes were
 * made.
 */
bool mm_vm_identity_map(struct mm_ptable *ptable, paddr_t begin, paddr_t end,
			mm_mode_t mode, struct mpool *ppool, ipaddr_t *ipa)
{
	struct mm_flags flags = mm_mode_to_flags(mode);
	bool success = mm_ptable_identity_update(
		ptable, begin, end, arch_mm_mode_to_stage2_attrs(mode), flags,
		ppool);

	if (success && ipa != NULL) {
		*ipa = ipa_from_pa(begin);
	}

	return success;
}

/**
 * Updates the VM's table such that the given physical address range has no
 * connection to the VM.
 */
bool mm_vm_unmap(struct mm_ptable *ptable, paddr_t begin, paddr_t end,
		 struct mpool *ppool)
{
	mm_mode_t mode = MM_MODE_UNMAPPED_MASK;

	return mm_vm_identity_map(ptable, begin, end, mode, ppool, NULL);
}

/**
 * Write the given page table of a VM to the debug log.
 */
void mm_vm_dump(const struct mm_ptable *ptable)
{
	mm_ptable_dump(ptable);
}

/**
 * Defragments a stage1 page table.
 */
void mm_stage1_defrag(struct mm_ptable *ptable, struct mpool *ppool)
{
	assert(ptable->stage1);
	mm_ptable_defrag(ptable, false, ppool);
}

/**
 * Defragments the VM page table.
 */
void mm_vm_defrag(struct mm_ptable *ptable, struct mpool *ppool,
		  bool non_secure)
{
	mm_ptable_defrag(ptable, non_secure, ppool);
}

/**
 * Gets the mode of the given range of intermediate physical addresses if they
 * are mapped with the same mode.
 *
 * Returns true if the range is mapped with the same mode and false otherwise.
 */
bool mm_vm_get_mode(const struct mm_ptable *ptable, ipaddr_t begin,
		    ipaddr_t end, mm_mode_t *mode)
{
	struct mm_get_attrs_state ret;
	bool success;

	ret = mm_get_attrs(ptable, ipa_addr(begin), ipa_addr(end));
	success = ret.got_attrs && !ret.got_mismatch;

	if (success && mode != NULL) {
		*mode = arch_mm_stage2_attrs_to_mode(ret.attrs);
	}

	return success;
}

bool mm_vm_get_mode_partial(const struct mm_ptable *ptable, ipaddr_t begin,
			    ipaddr_t end, mm_mode_t *mode, ipaddr_t *end_ret)
{
	struct mm_get_attrs_state ret;
	bool success;

	ret = mm_get_attrs(ptable, ipa_addr(begin), ipa_addr(end));
	success = ret.got_attrs;

	if (success && mode != NULL) {
		*mode = arch_mm_stage2_attrs_to_mode(ret.attrs);
	}

	if (success && end_ret != NULL) {
		*end_ret = ret.mismatch ? ipa_init(ret.mismatch) : end;
	}

	return success;
}

/**
 * Gets the mode of the given range of virtual addresses if they
 * are mapped with the same mode.
 *
 * Returns true if the range is mapped with the same mode and false otherwise.
 */
bool mm_get_mode(const struct mm_ptable *ptable, vaddr_t begin, vaddr_t end,
		 mm_mode_t *mode)
{
	struct mm_get_attrs_state ret;
	bool success;

	assert(ptable->stage1);

	ret = mm_get_attrs(ptable, va_addr(begin), va_addr(end));
	success = ret.got_attrs && !ret.got_mismatch;

	if (success && mode != NULL) {
		*mode = arch_mm_stage1_attrs_to_mode(ret.attrs);
	}

	return success;
}

bool mm_get_mode_partial(const struct mm_ptable *ptable, vaddr_t begin,
			 vaddr_t end, mm_mode_t *mode, vaddr_t *end_ret)
{
	struct mm_get_attrs_state ret;
	bool success;

	assert(ptable->stage1);

	ret = mm_get_attrs(ptable, va_addr(begin), va_addr(end));
	success = ret.got_attrs;

	if (success && mode != NULL) {
		*mode = arch_mm_stage1_attrs_to_mode(ret.attrs);
	}

	if (success && end_ret != NULL) {
		*end_ret = ret.mismatch ? va_init(ret.mismatch) : end;
	}

	return success;
}

static struct mm_stage1_locked mm_stage1_lock_unsafe(void)
{
	return (struct mm_stage1_locked){.ptable = &ptable};
}

struct mm_stage1_locked mm_lock_ptable_unsafe(struct mm_ptable *ptable)
{
	return (struct mm_stage1_locked){.ptable = ptable};
}

struct mm_stage1_locked mm_lock_stage1(void)
{
	sl_lock(&ptable_lock);
	return mm_stage1_lock_unsafe();
}

void mm_unlock_stage1(struct mm_stage1_locked *lock)
{
	CHECK(lock->ptable == &ptable);
	sl_unlock(&ptable_lock);
	lock->ptable = NULL;
}

/**
 * Updates the hypervisor page table such that the given physical address range
 * is mapped into the address space at the corresponding address range in the
 * architecture-agnostic mode provided.
 */
void *mm_identity_map(struct mm_stage1_locked stage1_locked, paddr_t begin,
		      paddr_t end, mm_mode_t mode, struct mpool *ppool)
{
	struct mm_flags flags = mm_mode_to_flags(mode);

	assert(stage1_locked.ptable->stage1);
	if (mm_ptable_identity_update(stage1_locked.ptable, begin, end,
				      arch_mm_mode_to_stage1_attrs(mode), flags,
				      ppool)) {
		return ptr_from_va(va_from_pa(begin));
	}

	return NULL;
}

/**
 * Updates the hypervisor table such that the given physical address range is
 * not mapped in the address space.
 */
bool mm_unmap(struct mm_stage1_locked stage1_locked, paddr_t begin, paddr_t end,
	      struct mpool *ppool)
{
	mm_mode_t mode = MM_MODE_UNMAPPED_MASK;

	return mm_identity_map(stage1_locked, begin, end, mode, ppool);
}

/**
 * Defragments the hypervisor page table.
 */
void mm_defrag(struct mm_stage1_locked stage1_locked, struct mpool *ppool)
{
	assert(stage1_locked.ptable->stage1);
	mm_ptable_defrag(stage1_locked.ptable, false, ppool);
}

/**
 * Initialises memory management for the hypervisor itself.
 */
bool mm_init(struct mpool *ppool)
{
	/* Locking is not enabled yet so fake it, */
	struct mm_stage1_locked stage1_locked = mm_stage1_lock_unsafe();

	dlog_info("text: %#lx - %#lx\n", pa_addr(layout_text_begin()),
		  pa_addr(layout_text_end()));
	dlog_info("rodata: %#lx - %#lx\n", pa_addr(layout_rodata_begin()),
		  pa_addr(layout_rodata_end()));
	dlog_info("data: %#lx - %#lx\n", pa_addr(layout_data_begin()),
		  pa_addr(layout_data_end()));
	dlog_info("stacks: %#lx - %#lx\n", pa_addr(layout_stacks_begin()),
		  pa_addr(layout_stacks_end()));

	/* ASID 0 is reserved for use by the hypervisor. */
	if (!mm_ptable_init(&ptable, 0, true, ppool)) {
		dlog_error("Unable to allocate memory for page table.\n");
		return false;
	}

	/* Initialize arch_mm before calling below mapping routines */
	if (!arch_mm_init(&ptable)) {
		return false;
	}

	/* Let console driver map pages for itself. */
	plat_console_mm_init(stage1_locked, ppool);

	/* Map each section. */
	CHECK(mm_identity_map(stage1_locked, layout_text_begin(),
			      layout_text_end(), MM_MODE_X, ppool) != NULL);

	CHECK(mm_identity_map(stage1_locked, layout_rodata_begin(),
			      layout_rodata_end(), MM_MODE_R, ppool) != NULL);

	CHECK(mm_identity_map(stage1_locked, layout_data_begin(),
			      layout_data_end(), MM_MODE_R | MM_MODE_W,
			      ppool) != NULL);

	/* Arch-specific stack mapping. */
	CHECK(arch_stack_mm_init(stage1_locked, ppool));

	return true;
}
