SPCI: support multiple constituents per memory region.

Previously the memory share mechanism only allowed for a contiguous
set of pages to be shared between two VMs in a single call. More
complex use-cases require non-contiguous pages to be shared between
end-points. This commit creates the necessary infrastructure to allow
non-contiguous pages to be shared in a single call.

Bug: 132420445
Change-Id: Ic9cbd9cb83abe7a55b02e4bfe07ec61e3d1b6822
diff --git a/inc/hf/spci_internal.h b/inc/hf/spci_internal.h
index 38bfd73..4f60a29 100644
--- a/inc/hf/spci_internal.h
+++ b/inc/hf/spci_internal.h
@@ -69,6 +69,7 @@
 
 bool spci_msg_check_transition(struct vm *to, struct vm *from,
 			       enum spci_memory_share share,
-			       uint32_t *orig_from_mode, ipaddr_t begin,
-			       ipaddr_t end, uint32_t memory_to_attributes,
+			       uint32_t *orig_from_mode,
+			       struct spci_memory_region *memory_region,
+			       uint32_t memory_to_attributes,
 			       uint32_t *from_mode, uint32_t *to_mode);
diff --git a/src/api.c b/src/api.c
index 163f3dd..8164cfb 100644
--- a/src/api.c
+++ b/src/api.c
@@ -1457,6 +1457,101 @@
 	return ret;
 }
 
+/**
+ * Clears a region of physical memory by overwriting it with zeros. The data is
+ * flushed from the cache so the memory has been cleared across the system.
+ */
+static bool api_clear_memory_region(struct spci_memory_region *memory_region)
+{
+	struct mpool local_page_pool;
+	struct spci_memory_region_constituent *constituents =
+		spci_memory_region_get_constituents(memory_region);
+	uint32_t memory_constituent_count = memory_region->constituent_count;
+	struct mm_stage1_locked stage1_locked;
+	bool ret = false;
+
+	/*
+	 * Create a local pool so any freed memory can't be used by another
+	 * thread. This is to ensure each constituent that is mapped can be
+	 * unmapped again afterwards.
+	 */
+	mpool_init_with_fallback(&local_page_pool, &api_page_pool);
+
+	/* Iterate over the memory region constituents. */
+	for (uint32_t i = 0; i < memory_constituent_count; ++i) {
+		size_t size = constituents[i].page_count * PAGE_SIZE;
+		paddr_t begin = pa_from_ipa(ipa_init(constituents[i].address));
+		paddr_t end = pa_add(begin, size);
+
+		if (!api_clear_memory(begin, end, &local_page_pool)) {
+			/*
+			 * api_clear_memory will defrag on failure, so no need
+			 * to do it here.
+			 */
+			goto out;
+		}
+	}
+
+	/*
+	 * Need to defrag after clearing, as it may have added extra mappings to
+	 * the stage 1 page table.
+	 */
+	stage1_locked = mm_lock_stage1();
+	mm_defrag(stage1_locked, &local_page_pool);
+	mm_unlock_stage1(&stage1_locked);
+
+	ret = true;
+
+out:
+	mpool_fini(&local_page_pool);
+	return ret;
+}
+
+/**
+ * Updates a VM's page table such that the given set of physical address ranges
+ * are mapped in the address space at the corresponding address ranges, in the
+ * mode provided.
+ *
+ * If commit is false, the page tables will be allocated from the mpool but no
+ * mappings will actually be updated. This function must always be called first
+ * with commit false to check that it will succeed before calling with commit
+ * true, to avoid leaving the page table in a half-updated state. To make a
+ * series of changes atomically you can call them all with commit false before
+ * calling them all with commit true.
+ *
+ * mm_vm_defrag should always be called after a series of page table updates,
+ * whether they succeed or fail.
+ *
+ * Returns true on success, or false if the update failed and no changes were
+ * made to memory mappings.
+ */
+static bool spci_region_group_identity_map(
+	struct mm_ptable *t, struct spci_memory_region *memory_region, int mode,
+	struct mpool *ppool, bool commit)
+{
+	struct spci_memory_region_constituent *constituents =
+		spci_memory_region_get_constituents(memory_region);
+	uint32_t memory_constituent_count = memory_region->constituent_count;
+
+	/* Iterate over the memory region constituents. */
+	for (uint32_t index = 0; index < memory_constituent_count; index++) {
+		size_t size = constituents[index].page_count * PAGE_SIZE;
+		paddr_t pa_begin =
+			pa_from_ipa(ipa_init(constituents[index].address));
+		paddr_t pa_end = pa_add(pa_begin, size);
+
+		if (commit) {
+			mm_vm_identity_commit(t, pa_begin, pa_end, mode, ppool,
+					      NULL);
+		} else if (!mm_vm_identity_prepare(t, pa_begin, pa_end, mode,
+						   ppool)) {
+			return false;
+		}
+	}
+
+	return true;
+}
+
 /** TODO: Move function to spci_architected_message.c. */
 /**
  * Shares memory from the calling VM with another. The memory can be shared in
@@ -1484,15 +1579,9 @@
 	uint32_t to_mode;
 	struct mpool local_page_pool;
 	struct spci_value ret;
-	paddr_t pa_begin;
-	paddr_t pa_end;
-	ipaddr_t begin;
-	ipaddr_t end;
 	struct spci_memory_region_constituent *constituents =
 		spci_memory_region_get_constituents(memory_region);
 
-	size_t size;
-
 	/*
 	 * Make sure constituents are properly aligned to a 64-bit boundary. If
 	 * not we would get alignment faults trying to read (64-bit) page
@@ -1508,76 +1597,85 @@
 	}
 
 	/*
-	 * Create a local pool so any freed memory can't be used by another
-	 * thread. This is to ensure the original mapping can be restored if any
-	 * stage of the process fails.
-	 */
-	mpool_init_with_fallback(&local_page_pool, &api_page_pool);
-
-	/* Obtain the single contiguous set of pages from the memory_region. */
-	/* TODO: Add support for multiple constituent regions. */
-	size = constituents[0].page_count * PAGE_SIZE;
-	begin = ipa_init(constituents[0].address);
-	end = ipa_add(begin, size);
-
-	/*
 	 * Check if the state transition is lawful for both VMs involved
 	 * in the memory exchange, ensure that all constituents of a memory
 	 * region being shared are at the same state.
 	 */
-	if (!spci_msg_check_transition(to, from, share, &orig_from_mode, begin,
-				       end, memory_to_attributes, &from_mode,
-				       &to_mode)) {
+	if (!spci_msg_check_transition(to, from, share, &orig_from_mode,
+				       memory_region, memory_to_attributes,
+				       &from_mode, &to_mode)) {
 		return spci_error(SPCI_INVALID_PARAMETERS);
 	}
 
-	pa_begin = pa_from_ipa(begin);
-	pa_end = pa_from_ipa(end);
+	/*
+	 * Create a local pool so any freed memory can't be used by another
+	 * thread. This is to ensure the original mapping can be restored if the
+	 * clear fails.
+	 */
+	mpool_init_with_fallback(&local_page_pool, &api_page_pool);
 
 	/*
-	 * First update the mapping for the sender so there is not overlap with
-	 * the recipient.
+	 * First reserve all required memory for the new page table entries in
+	 * both sender and recipient page tables without committing, to make
+	 * sure the entire operation will succeed without exhausting the page
+	 * pool.
 	 */
-	if (!mm_vm_identity_map(&from->ptable, pa_begin, pa_end, from_mode,
-				&local_page_pool, NULL)) {
+	if (!spci_region_group_identity_map(&from->ptable, memory_region,
+					    from_mode, &api_page_pool, false) ||
+	    !spci_region_group_identity_map(&to->ptable, memory_region, to_mode,
+					    &api_page_pool, false)) {
+		/* TODO: partial defrag of failed range. */
 		ret = spci_error(SPCI_NO_MEMORY);
 		goto out;
 	}
 
+	/*
+	 * First update the mapping for the sender so there is no overlap with
+	 * the recipient. This won't allocate because the transaction was
+	 * already prepared above, but may free pages in the case that a whole
+	 * block is being unmapped that was previously partially mapped.
+	 */
+	CHECK(spci_region_group_identity_map(&from->ptable, memory_region,
+					     from_mode, &local_page_pool,
+					     true));
+
 	/* Clear the memory so no VM or device can see the previous contents. */
 	if ((memory_region->flags & SPCI_MEMORY_REGION_FLAG_CLEAR) &&
-	    !api_clear_memory(pa_begin, pa_end, &local_page_pool)) {
+	    !api_clear_memory_region(memory_region)) {
+		/*
+		 * On failure, roll back by returning memory to the sender. This
+		 * may allocate pages which were previously freed into
+		 * `local_page_pool` by the call above, but will never allocate
+		 * more pages than that so can never fail.
+		 */
+		CHECK(spci_region_group_identity_map(
+			&from->ptable, memory_region, orig_from_mode,
+			&local_page_pool, true));
+
 		ret = spci_error(SPCI_NO_MEMORY);
-
-		/* Return memory to the sender. */
-		CHECK(mm_vm_identity_map(&from->ptable, pa_begin, pa_end,
-					 orig_from_mode, &local_page_pool,
-					 NULL));
-
 		goto out;
 	}
 
-	/* Complete the transfer by mapping the memory into the recipient. */
-	if (!mm_vm_identity_map(&to->ptable, pa_begin, pa_end, to_mode,
-				&local_page_pool, NULL)) {
-		/* TODO: partial defrag of failed range. */
-		/* Recover any memory consumed in failed mapping. */
-		mm_vm_defrag(&from->ptable, &local_page_pool);
-
-		ret = spci_error(SPCI_NO_MEMORY);
-
-		CHECK(mm_vm_identity_map(&from->ptable, pa_begin, pa_end,
-					 orig_from_mode, &local_page_pool,
-					 NULL));
-
-		goto out;
-	}
+	/*
+	 * Complete the transfer by mapping the memory into the recipient. This
+	 * won't allocate because the transaction was already prepared above, so
+	 * it doesn't need to use the `local_page_pool`.
+	 */
+	CHECK(spci_region_group_identity_map(&to->ptable, memory_region,
+					     to_mode, &api_page_pool, true));
 
 	ret = (struct spci_value){.func = SPCI_SUCCESS_32};
 
 out:
 	mpool_fini(&local_page_pool);
 
+	/*
+	 * Tidy up the page tables by reclaiming failed mappings (if there was
+	 * an error) or merging entries into blocks where possible (on success).
+	 */
+	mm_vm_defrag(&to->ptable, &api_page_pool);
+	mm_vm_defrag(&from->ptable, &api_page_pool);
+
 	return ret;
 }
 
diff --git a/src/spci_architected_message.c b/src/spci_architected_message.c
index f359b08..9a32d75 100644
--- a/src/spci_architected_message.c
+++ b/src/spci_architected_message.c
@@ -118,9 +118,7 @@
 /**
  * Obtain the next mode to apply to the two VMs.
  *
- * Returns:
- *  The error code -1 indicates that a state transition was not found.
- *  Success is indicated by 0.
+ * Returns true iff a state transition was found.
  */
 static bool spci_msg_get_next_state(
 	const struct spci_mem_transitions *transitions,
@@ -168,13 +166,15 @@
  */
 bool spci_msg_check_transition(struct vm *to, struct vm *from,
 			       enum spci_memory_share share,
-			       uint32_t *orig_from_mode, ipaddr_t begin,
-			       ipaddr_t end, uint32_t memory_to_attributes,
+			       uint32_t *orig_from_mode,
+			       struct spci_memory_region *memory_region,
+			       uint32_t memory_to_attributes,
 			       uint32_t *from_mode, uint32_t *to_mode)
 {
 	uint32_t orig_to_mode;
 	const struct spci_mem_transitions *mem_transition_table;
 	uint32_t transition_table_size;
+	uint32_t i;
 
 	/*
 	 * TODO: Transition table does not currently consider the multiple
@@ -308,16 +308,52 @@
 	static const uint32_t size_relinquish_transitions =
 		ARRAY_SIZE(relinquish_transitions);
 
-	/* Fail if addresses are not page-aligned. */
-	if (!is_aligned(ipa_addr(begin), PAGE_SIZE) ||
-	    !is_aligned(ipa_addr(end), PAGE_SIZE)) {
+	struct spci_memory_region_constituent *constituents =
+		spci_memory_region_get_constituents(memory_region);
+
+	if (memory_region->constituent_count == 0) {
+		/*
+		 * Fail if there are no constituents. Otherwise
+		 * spci_msg_get_next_state would get an unitialised
+		 * *orig_from_mode and orig_to_mode.
+		 */
 		return false;
 	}
 
-	/* Ensure that the memory range is mapped with the same mode. */
-	if (!mm_vm_get_mode(&from->ptable, begin, end, orig_from_mode) ||
-	    !mm_vm_get_mode(&to->ptable, begin, end, &orig_to_mode)) {
-		return false;
+	for (i = 0; i < memory_region->constituent_count; ++i) {
+		ipaddr_t begin = ipa_init(constituents[i].address);
+		size_t size = constituents[i].page_count * PAGE_SIZE;
+		ipaddr_t end = ipa_add(begin, size);
+		uint32_t current_from_mode;
+		uint32_t current_to_mode;
+
+		/* Fail if addresses are not page-aligned. */
+		if (!is_aligned(ipa_addr(begin), PAGE_SIZE) ||
+		    !is_aligned(ipa_addr(end), PAGE_SIZE)) {
+			return false;
+		}
+
+		/*
+		 * Ensure that this constituent memory range is all mapped with
+		 * the same mode.
+		 */
+		if (!mm_vm_get_mode(&from->ptable, begin, end,
+				    &current_from_mode) ||
+		    !mm_vm_get_mode(&to->ptable, begin, end,
+				    &current_to_mode)) {
+			return false;
+		}
+
+		/*
+		 * Ensure that all constituents are mapped with the same mode.
+		 */
+		if (i == 0) {
+			*orig_from_mode = current_from_mode;
+			orig_to_mode = current_to_mode;
+		} else if (current_from_mode != *orig_from_mode ||
+			   current_to_mode != orig_to_mode) {
+			return false;
+		}
 	}
 
 	/* Ensure the address range is normal memory and not a device. */