Introduce a DT-based manifest

These are first steps towards a new manifest format. A new "device_tree"
build target is introduced to compile DTS files to DTB, and
`generate_initrd.py` now does not produce a "vms.txt" file. Instead
"initrd" targets are expected to provide a path to a DTS manifest in the
format:

    /dts-v1/;

    / {
      hypervisor {
        vm1 {
	  debug_name = "primary";
	};

	vm2 {
	  debug_name = "secondary1";
	  kernel_filename = "filename";
	  vcpu_count = <N>;
          mem_size = <M>;
	};

	...
      };
    };

The information provided in the manifest matches "vms.txt".

Bug: 117551352
Test: manifest_test.cc
Test: used by hftest
Change-Id: I6b70bd44d2b110c4f7a6b971018c834084b6d8c4
diff --git a/Makefile b/Makefile
index b37d946..2d3a885 100644
--- a/Makefile
+++ b/Makefile
@@ -39,7 +39,7 @@
 export PATH := $(PREBUILTS)/clang/bin:$(PATH)
 
 CHECKPATCH := $(PWD)/third_party/linux/scripts/checkpatch.pl \
-	--ignore BRACES,SPDX_LICENSE_TAG,VOLATILE,SPLIT_STRING,AVOID_EXTERNS,USE_SPINLOCK_T,NEW_TYPEDEFS,INITIALISED_STATIC,FILE_PATH_CHANGES,EMBEDDED_FUNCTION_NAME,SINGLE_STATEMENT_DO_WHILE_MACRO --quiet
+	--ignore BRACES,SPDX_LICENSE_TAG,VOLATILE,SPLIT_STRING,AVOID_EXTERNS,USE_SPINLOCK_T,NEW_TYPEDEFS,INITIALISED_STATIC,FILE_PATH_CHANGES,EMBEDDED_FUNCTION_NAME,SINGLE_STATEMENT_DO_WHILE_MACRO,MACRO_WITH_FLOW_CONTROL --quiet
 
 # Select the project to build.
 PROJECT ?= reference
@@ -99,9 +99,9 @@
 
 .PHONY: license
 license:
-	@find src/ -name \*.S -o -name \*.c -o -name \*.cc -o -name \*.h | xargs -n1 python build/license.py --style c
-	@find inc/ -name \*.S -o -name \*.c -o -name \*.cc -o -name \*.h | xargs -n1 python build/license.py --style c
-	@find test/ -name \*.S -o -name \*.c -o -name \*.cc -o -name \*.h | xargs -n1 python build/license.py --style c
+	@find src/ -name \*.S -o -name \*.c -o -name \*.cc -o -name \*.h -o -name \*.dts | xargs -n1 python build/license.py --style c
+	@find inc/ -name \*.S -o -name \*.c -o -name \*.cc -o -name \*.h -o -name \*.dts | xargs -n1 python build/license.py --style c
+	@find test/ -name \*.S -o -name \*.c -o -name \*.cc -o -name \*.h -o -name \*.dts | xargs -n1 python build/license.py --style c
 	@find build/ -name \*.py| xargs -n1 python build/license.py --style hash
 	@find test/ -name \*.py| xargs -n1 python build/license.py --style hash
 	@find . \( -name \*.gn -o -name \*.gni \) | xargs -n1 python build/license.py --style hash
diff --git a/build/image/dtc.py b/build/image/dtc.py
new file mode 100644
index 0000000..49eeeb7
--- /dev/null
+++ b/build/image/dtc.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+#
+# Copyright 2019 The Hafnium Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Wrapper around Device Tree Compiler (dtc)"""
+
+import argparse
+import os
+import subprocess
+import sys
+
+DTC = "dtc"
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("input_file")
+    parser.add_argument("output_file")
+    args = parser.parse_args()
+
+    return subprocess.call([
+        DTC,
+        "-I", "dts", "-O", "dtb",
+        "-o", args.output_file,
+        "--out-version", "17",
+        args.input_file
+        ])
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/build/image/generate_initrd.py b/build/image/generate_initrd.py
index 6fb76de..8342cfb 100644
--- a/build/image/generate_initrd.py
+++ b/build/image/generate_initrd.py
@@ -29,17 +29,27 @@
 
 def Main():
     parser = argparse.ArgumentParser()
+    parser.add_argument("--manifest", required=True)
     parser.add_argument("--primary_vm", required=True)
     parser.add_argument("--primary_vm_initrd")
     parser.add_argument(
         "--secondary_vm",
         action="append",
-        nargs=4,
-        metavar=("MEMORY", "CORES", "NAME", "IMAGE"))
+        nargs=2,
+        metavar=("NAME", "IMAGE"))
     parser.add_argument("--staging", required=True)
     parser.add_argument("--output", required=True)
     args = parser.parse_args()
     staged_files = ["vmlinuz", "initrd.img"]
+
+    # Create staging folder if needed.
+    if not os.path.isdir(args.staging):
+        os.makedirs(args.staging)
+
+    # Prepare the manifest.
+    if args.manifest:
+        shutil.copyfile(args.manifest, os.path.join(args.staging, "manifest.dtb"))
+        staged_files += ["manifest.dtb"]
     # Prepare the primary VM image.
     shutil.copyfile(args.primary_vm, os.path.join(args.staging, "vmlinuz"))
     # Prepare the primary VM's initrd.
@@ -48,14 +58,11 @@
     else:
         open(os.path.join(args.staging, "initrd.img"), "w").close()
     # Prepare the secondary VMs.
-    with open(os.path.join(args.staging, "vms.txt"), "w") as vms_txt:
-        staged_files.append("vms.txt")
-        if args.secondary_vm:
-            for vm in args.secondary_vm:
-                (vm_memory, vm_cores, vm_name, vm_image) = vm
-                staged_files.append(vm_name)
-                shutil.copy(vm_image, os.path.join(args.staging, vm_name))
-                vms_txt.write("{} {} {}\n".format(vm_memory, vm_cores, vm_name))
+    if args.secondary_vm:
+        for vm in args.secondary_vm:
+            (vm_name, vm_image) = vm
+            staged_files.append(vm_name)
+            shutil.copy(vm_image, os.path.join(args.staging, vm_name))
     # Package files into an initial RAM disk.
     with open(args.output, "w") as initrd:
         # Move into the staging directory so the file names taken by cpio don't
diff --git a/build/image/image.gni b/build/image/image.gni
index fbaf14b..bed73df 100644
--- a/build/image/image.gni
+++ b/build/image/image.gni
@@ -148,11 +148,41 @@
   }
 }
 
+template("device_tree") {
+  action_foreach(target_name) {
+    forward_variables_from(invoker,
+                           [
+                             "testonly",
+                             "sources",
+                             "deps",
+                           ])
+    script = "//build/image/dtc.py"
+
+    dtb_file = "${target_out_dir}/{{source_name_part}}.dtb"
+
+    outputs = [
+      dtb_file,
+    ]
+    args = [
+      "{{source}}",
+      rebase_path(dtb_file),
+    ]
+  }
+}
+
 # Build the initial RAM disk for the hypervisor.
 template("initrd") {
   assert(defined(invoker.primary_vm),
          "initrd() must specify a \"primary_vm\" value")
 
+  manifest_target = "${target_name}__manifest"
+
+  device_tree(manifest_target) {
+    sources = [
+      invoker.manifest,
+    ]
+  }
+
   action(target_name) {
     forward_variables_from(invoker, [ "testonly" ])
     script = "//build/image/generate_initrd.py"
@@ -189,27 +219,29 @@
     # Add the info about the secondary VMs. The information about the VMs is
     # encoded in lists with the following elements:
     #
-    #    1. Memory in bytes.
-    #    2. Number of cores.
-    #    3. File name for the VM image.
-    #    4. Build target for the VM.
+    #    1. File name for the VM image.
+    #    2. Build target for the VM.
     if (defined(invoker.secondary_vms)) {
       foreach(vm, invoker.secondary_vms) {
-        deps += [ vm[3] ]
+        deps += [ vm[1] ]
         args += [
           "--secondary_vm",
           vm[0],
-          vm[1],
-          vm[2],
-          rebase_path(get_label_info(vm[3], "target_out_dir") + "/" +
-                      get_label_info(vm[3], "name") + ".bin"),
+          rebase_path(get_label_info(vm[1], "target_out_dir") + "/" +
+                      get_label_info(vm[1], "name") + ".bin"),
         ]
       }
     }
 
+    manifest_target_outputs = get_target_outputs(":${manifest_target}")
+    deps += [ ":${manifest_target}" ]
+    args += [
+      "--manifest",
+      rebase_path(manifest_target_outputs[0]),
+    ]
+
     outputs = [
       initrd_file,
-      "${initrd_staging}/vms.txt",
     ]
   }
 }
diff --git a/docs/HafniumRamDisk.md b/docs/HafniumRamDisk.md
index 1a537f1..37e27db 100644
--- a/docs/HafniumRamDisk.md
+++ b/docs/HafniumRamDisk.md
@@ -5,27 +5,69 @@
 
 *   `vmlinuz` -- the kernel of the primary VM.
 *   `initrd.img` -- the initial ramdisk of the primary VM.
-*   `vms.txt` -- optionally describes the secondary VMs.
-*   kernels for the secondary VMs, whose names are described in `vms.txt`.
+*   `manifest.dtb` -- hypervisor configuration file.
+*   kernels for the secondary VMs, whose names are described in the manifest.
 
 Follow the [preparing Linux](PreparingLinux.md) instructions to produce
 `vmlinuz` and `initrd.img` for a basic Linux primary VM.
 
-## Format of `vms.txt` file
+## Manifest file
 
-The format is currently one line per secondary VM, with the following format:
+The format is currently a simple Device Tree:
 
-```shell
-<memory-size-in-bytes> <number-of-cpus> <kernel-filename>
+```
+/dts-v1/;
+
+/ {
+	hypervisor {
+		vm1 {
+			debug_name = "name";
+		};
+
+		vm2 {
+			debug_name = "name";
+			kernel_filename = "filename";
+			vcpu_count = <N>;
+			mem_size = <M>;
+		};
+		...
+	};
+};
 ```
 
 For example, the following defines two secondary VMs, the first one with 1MB of
 memory, 2 CPUs and kernel image called `kernel0`, while the second one has 2MB
 of memory, 4 CPUs and a kernel image called `kernel1`.
 
+```
+/dts-v1/;
+
+/ {
+	hypervisor {
+		vm1 {
+			debug_name = "primary VM";
+		};
+
+		vm2 {
+			debug_name = "secondary VM 1";
+			kernel_filename = "kernel0";
+			vcpu_count = <2>;
+			mem_size = <0x100000>;
+		};
+
+		vm3 {
+			debug_name = "secondary VM 2";
+			kernel_filename = "kernel1";
+			vcpu_count = <4>;
+			mem_size = <0x200000>;
+		};
+	};
+};
+```
+
+Hafnium expects the manifest in Device Tree Blob format. Compile it with:
 ```shell
-1048576 2 kernel0
-2097152 4 kernel1
+dtc -I dts -O dtb --out-version 17 -o manifest.dtb <manifest_source_file>
 ```
 
 ## Create a RAM disk for Hafnium
diff --git a/inc/hf/fdt.h b/inc/hf/fdt.h
index 2f55306..8f72856 100644
--- a/inc/hf/fdt.h
+++ b/inc/hf/fdt.h
@@ -29,13 +29,14 @@
 
 size_t fdt_header_size(void);
 uint32_t fdt_total_size(struct fdt_header *hdr);
-void fdt_dump(struct fdt_header *hdr);
+void fdt_dump(const struct fdt_header *hdr);
 bool fdt_root_node(struct fdt_node *node, const struct fdt_header *hdr);
 bool fdt_find_child(struct fdt_node *node, const char *child);
 bool fdt_first_child(struct fdt_node *node, const char **child_name);
 bool fdt_next_sibling(struct fdt_node *node, const char **sibling_name);
 bool fdt_read_property(const struct fdt_node *node, const char *name,
 		       const char **buf, uint32_t *size);
+bool fdt_parse_number(const char *data, uint32_t size, uint64_t *value);
 
 void fdt_add_mem_reservation(struct fdt_header *hdr, uint64_t addr,
 			     uint64_t len);
diff --git a/inc/hf/manifest.h b/inc/hf/manifest.h
new file mode 100644
index 0000000..37f26f3
--- /dev/null
+++ b/inc/hf/manifest.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2019 The Hafnium Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "hf/memiter.h"
+#include "hf/spci.h"
+
+/**
+ * Holds information about one of the VMs described in the manifest.
+ */
+struct manifest_vm {
+	/* Properties defined for both primary and secondary VMs. */
+	struct memiter debug_name;
+
+	/* Properties specific to secondary VMs. */
+	struct {
+		struct memiter kernel_filename;
+		uint64_t mem_size;
+		spci_vcpu_count_t vcpu_count;
+	} secondary;
+};
+
+/**
+ * Hafnium manifest parsed from FDT.
+ */
+struct manifest {
+	spci_vm_count_t num_vms;
+	struct manifest_vm vm[MAX_VMS];
+};
+
+enum manifest_return_code {
+	MANIFEST_SUCCESS = 0,
+	MANIFEST_ERROR_CORRUPTED_FDT,
+	MANIFEST_ERROR_NO_ROOT_FDT_NODE,
+	MANIFEST_ERROR_NO_HYPERVISOR_FDT_NODE,
+	MANIFEST_ERROR_RESERVED_VM_ID,
+	MANIFEST_ERROR_NO_PRIMARY_VM,
+	MANIFEST_ERROR_TOO_MANY_VMS,
+	MANIFEST_ERROR_PROPERTY_NOT_FOUND,
+	MANIFEST_ERROR_MALFORMED_STRING,
+	MANIFEST_ERROR_MALFORMED_INTEGER,
+	MANIFEST_ERROR_INTEGER_OVERFLOW,
+};
+
+enum manifest_return_code manifest_init(struct manifest *manifest,
+					struct memiter *fdt);
+
+const char *manifest_strerror(enum manifest_return_code ret_code);
diff --git a/inc/hf/memiter.h b/inc/hf/memiter.h
index d5dbf33..a44ffef 100644
--- a/inc/hf/memiter.h
+++ b/inc/hf/memiter.h
@@ -29,4 +29,8 @@
 bool memiter_parse_uint(struct memiter *it, uint64_t *value);
 bool memiter_parse_str(struct memiter *it, struct memiter *str);
 bool memiter_iseq(const struct memiter *it, const char *str);
+void memiter_dlog_str(struct memiter *it);
 bool memiter_advance(struct memiter *it, size_t v);
+
+const void *memiter_base(struct memiter *it);
+size_t memiter_size(struct memiter *it);
diff --git a/src/BUILD.gn b/src/BUILD.gn
index 8c5b6e5..f6f55dc 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -48,6 +48,7 @@
     "abort.c",
     "api.c",
     "cpu.c",
+    "manifest.c",
     "panic.c",
     "spci_architected_message.c",
     "vm.c",
@@ -151,6 +152,7 @@
     "api_test.cc",
     "fdt_handler_test.cc",
     "fdt_test.cc",
+    "manifest_test.cc",
     "mm_test.cc",
     "mpool_test.cc",
     "spci_test.cc",
diff --git a/src/fdt.c b/src/fdt.c
index 0ba1177..d9cd71d 100644
--- a/src/fdt.c
+++ b/src/fdt.c
@@ -16,8 +16,10 @@
 
 #include "hf/fdt.h"
 
+#include <stdalign.h>
 #include <stdint.h>
 
+#include "hf/check.h"
 #include "hf/dlog.h"
 #include "hf/std.h"
 
@@ -56,6 +58,8 @@
 #define FDT_VERSION 17
 #define FDT_MAGIC 0xd00dfeed
 
+#define FDT_TOKEN_ALIGNMENT sizeof(uint32_t)
+
 static void fdt_tokenizer_init(struct fdt_tokenizer *t, const char *strs,
 			       const char *begin, const char *end)
 {
@@ -66,7 +70,7 @@
 
 static void fdt_tokenizer_align(struct fdt_tokenizer *t)
 {
-	t->cur = (char *)align_up(t->cur, 4);
+	t->cur = (char *)align_up(t->cur, FDT_TOKEN_ALIGNMENT);
 }
 
 static bool fdt_tokenizer_uint32(struct fdt_tokenizer *t, uint32_t *res)
@@ -276,6 +280,43 @@
 	return false;
 }
 
+/**
+ * Helper method for parsing 32/64-bit uints from FDT data.
+ */
+bool fdt_parse_number(const char *data, uint32_t size, uint64_t *value)
+{
+	union {
+		volatile uint64_t v;
+		char a[8];
+	} t;
+
+	/* FDT values should be aligned to 32-bit boundary. */
+	CHECK(is_aligned(data, FDT_TOKEN_ALIGNMENT));
+
+	switch (size) {
+	case sizeof(uint32_t):
+		/*
+		 * Assert that `data` is already sufficiently aligned to
+		 * dereference as uint32_t. We cannot use static_assert()
+		 * because alignof() is not an expression under ISO C11.
+		 */
+		CHECK(alignof(uint32_t) <= FDT_TOKEN_ALIGNMENT);
+		*value = be32toh(*(uint32_t *)data);
+		return true;
+	case sizeof(uint64_t):
+		/*
+		 * ARMv8 requires `data` to be realigned to 64-bit boundary
+		 * to dereference as uint64_t. May not be needed on other
+		 * architectures.
+		 */
+		memcpy_s(t.a, sizeof(t.a), data, sizeof(uint64_t));
+		*value = be64toh(t.v);
+		return true;
+	default:
+		return false;
+	}
+}
+
 bool fdt_first_child(struct fdt_node *node, const char **child_name)
 {
 	struct fdt_tokenizer t;
@@ -333,7 +374,7 @@
 	return false;
 }
 
-void fdt_dump(struct fdt_header *hdr)
+void fdt_dump(const struct fdt_header *hdr)
 {
 	uint32_t token;
 	size_t depth = 0;
diff --git a/src/fdt_handler.c b/src/fdt_handler.c
index db8c0ef..900f766 100644
--- a/src/fdt_handler.c
+++ b/src/fdt_handler.c
@@ -17,6 +17,7 @@
 #include "hf/fdt_handler.h"
 
 #include "hf/boot_params.h"
+#include "hf/check.h"
 #include "hf/cpu.h"
 #include "hf/dlog.h"
 #include "hf/fdt.h"
@@ -24,24 +25,6 @@
 #include "hf/mm.h"
 #include "hf/std.h"
 
-static uint64_t convert_number(const char *data, uint32_t size)
-{
-	union {
-		volatile uint64_t v;
-		char a[8];
-	} t;
-
-	switch (size) {
-	case sizeof(uint32_t):
-		return be32toh(*(uint32_t *)data);
-	case sizeof(uint64_t):
-		memcpy_s(t.a, sizeof(t.a), data, sizeof(uint64_t));
-		return be64toh(t.v);
-	default:
-		return 0;
-	}
-}
-
 static bool fdt_read_number(const struct fdt_node *node, const char *name,
 			    uint64_t *value)
 {
@@ -55,7 +38,7 @@
 	switch (size) {
 	case sizeof(uint32_t):
 	case sizeof(uint64_t):
-		*value = convert_number(data, size);
+		CHECK(fdt_parse_number(data, size, value));
 		break;
 
 	default:
@@ -163,13 +146,18 @@
 
 		/* Get all entries for this CPU. */
 		while (size >= address_size) {
+			uint64_t value;
+
 			if (*cpu_count >= MAX_CPUS) {
 				dlog("Found more than %d CPUs\n", MAX_CPUS);
 				return;
 			}
 
-			cpu_ids[(*cpu_count)++] =
-				convert_number(data, address_size);
+			if (!fdt_parse_number(data, address_size, &value)) {
+				dlog("Could not parse CPU id\n");
+				return;
+			}
+			cpu_ids[(*cpu_count)++] = value;
 
 			size -= address_size;
 			data += address_size;
@@ -219,9 +207,12 @@
 
 		/* Traverse all memory ranges within this node. */
 		while (size >= entry_size) {
-			uintpaddr_t addr = convert_number(data, address_size);
-			size_t len =
-				convert_number(data + address_size, size_size);
+			uintpaddr_t addr;
+			size_t len;
+
+			CHECK(fdt_parse_number(data, address_size, &addr));
+			CHECK(fdt_parse_number(data + address_size, size_size,
+					       &len));
 
 			if (mem_range_index < MAX_MEM_RANGES) {
 				p->mem_ranges[mem_range_index].begin =
diff --git a/src/load.c b/src/load.c
index 602cfa8..341f276 100644
--- a/src/load.c
+++ b/src/load.c
@@ -22,6 +22,7 @@
 #include "hf/boot_params.h"
 #include "hf/dlog.h"
 #include "hf/layout.h"
+#include "hf/manifest.h"
 #include "hf/memiter.h"
 #include "hf/mm.h"
 #include "hf/plat/console.h"
@@ -40,8 +41,10 @@
  * so the data must be available without the cache.
  */
 static bool copy_to_unmapped(struct mm_stage1_locked stage1_locked, paddr_t to,
-			     const void *from, size_t size, struct mpool *ppool)
+			     struct memiter *from_it, struct mpool *ppool)
 {
+	const void *from = memiter_base(from_it);
+	size_t size = memiter_size(from_it);
 	paddr_t to_end = pa_add(to, size);
 	void *ptr;
 
@@ -120,8 +123,7 @@
 	}
 
 	dlog("Copying primary to %p\n", pa_addr(primary_begin));
-	if (!copy_to_unmapped(stage1_locked, primary_begin, it.next,
-			      it.limit - it.next, ppool)) {
+	if (!copy_to_unmapped(stage1_locked, primary_begin, &it, ppool)) {
 		dlog("Unable to relocate kernel for primary vm.\n");
 		return false;
 	}
@@ -254,12 +256,11 @@
 		    struct boot_params_update *update, struct mpool *ppool)
 {
 	struct vm *primary;
-	struct memiter it;
-	struct memiter name;
-	uint64_t mem;
-	uint64_t cpu;
+	struct manifest manifest;
+	struct memiter manifest_fdt;
 	struct mem_range mem_ranges_available[MAX_MEM_RANGES];
 	size_t i;
+	enum manifest_return_code manifest_ret;
 
 	static_assert(
 		sizeof(mem_ranges_available) == sizeof(params->mem_ranges),
@@ -272,61 +273,74 @@
 
 	primary = vm_find(HF_PRIMARY_VM_ID);
 
-	if (!find_file(cpio, "vms.txt", &it)) {
-		dlog("vms.txt is missing\n");
-		return true;
-	}
-
 	/* Round the last addresses down to the page size. */
 	for (i = 0; i < params->mem_ranges_count; ++i) {
 		mem_ranges_available[i].end = pa_init(align_down(
 			pa_addr(mem_ranges_available[i].end), PAGE_SIZE));
 	}
 
-	while (memiter_parse_uint(&it, &mem) && memiter_parse_uint(&it, &cpu) &&
-	       memiter_parse_str(&it, &name)) {
+	if (!find_file(cpio, "manifest.dtb", &manifest_fdt)) {
+		dlog("Could not find \"manifest.dtb\" in cpio.\n");
+		return false;
+	}
+
+	manifest_ret = manifest_init(&manifest, &manifest_fdt);
+	if (manifest_ret != MANIFEST_SUCCESS) {
+		dlog("Could not parse manifest: %s.\n",
+		     manifest_strerror(manifest_ret));
+		return false;
+	}
+
+	for (i = 0; i < manifest.num_vms; ++i) {
+		struct manifest_vm *manifest_vm = &manifest.vm[i];
+		spci_vm_id_t vm_id = HF_VM_ID_OFFSET + i;
+		struct vm *vm;
+		struct vcpu *vcpu;
 		struct memiter kernel;
+		uint64_t mem_size;
 		paddr_t secondary_mem_begin;
 		paddr_t secondary_mem_end;
 		ipaddr_t secondary_entry;
-		const char *p;
-		struct vm *vm;
-		struct vcpu *vcpu;
 
-		dlog("Loading ");
-		for (p = name.next; p != name.limit; ++p) {
-			dlog("%c", *p);
-		}
-		dlog("\n");
-
-		if (!memiter_find_file(cpio, &name, &kernel)) {
-			dlog("Unable to load kernel\n");
+		if (vm_id == HF_PRIMARY_VM_ID) {
 			continue;
 		}
 
-		/* Round up to page size. */
-		mem = (mem + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);
+		dlog("Loading VM%d: ", (int)vm_id);
+		memiter_dlog_str(&manifest_vm->debug_name);
+		dlog(".\n");
 
-		if (mem < kernel.limit - kernel.next) {
+		if (!memiter_find_file(cpio,
+				       &manifest_vm->secondary.kernel_filename,
+				       &kernel)) {
+			dlog("Could not find kernel file \"");
+			memiter_dlog_str(
+				&manifest_vm->secondary.kernel_filename);
+			dlog("\".\n");
+			continue;
+		}
+
+		mem_size = align_up(manifest_vm->secondary.mem_size, PAGE_SIZE);
+		if (mem_size < memiter_size(&kernel)) {
 			dlog("Kernel is larger than available memory\n");
 			continue;
 		}
 
-		if (!carve_out_mem_range(
-			    mem_ranges_available, params->mem_ranges_count, mem,
-			    &secondary_mem_begin, &secondary_mem_end)) {
-			dlog("Not enough memory (%u bytes)\n", mem);
+		if (!carve_out_mem_range(mem_ranges_available,
+					 params->mem_ranges_count, mem_size,
+					 &secondary_mem_begin,
+					 &secondary_mem_end)) {
+			dlog("Not enough memory (%u bytes)\n", mem_size);
 			continue;
 		}
 
 		if (!copy_to_unmapped(stage1_locked, secondary_mem_begin,
-				      kernel.next, kernel.limit - kernel.next,
-				      ppool)) {
+				      &kernel, ppool)) {
 			dlog("Unable to copy kernel\n");
 			continue;
 		}
 
-		if (!vm_init(cpu, ppool, &vm)) {
+		if (!vm_init(manifest_vm->secondary.vcpu_count, ppool, &vm)) {
 			dlog("Unable to initialise VM\n");
 			continue;
 		}
@@ -347,7 +361,8 @@
 			return false;
 		}
 
-		dlog("Loaded with %u vcpus, entry at %#x\n", cpu,
+		dlog("Loaded with %u vcpus, entry at %#x\n",
+		     manifest_vm->secondary.vcpu_count,
 		     pa_addr(secondary_mem_begin));
 
 		vcpu = vm_get_vcpu(vm, 0);
diff --git a/src/manifest.c b/src/manifest.c
new file mode 100644
index 0000000..e5c62a1
--- /dev/null
+++ b/src/manifest.c
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2019 The Hafnium Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hf/manifest.h"
+
+#include "hf/addr.h"
+#include "hf/check.h"
+#include "hf/fdt.h"
+#include "hf/static_assert.h"
+#include "hf/std.h"
+
+#define TRY(expr)                                            \
+	do {                                                 \
+		enum manifest_return_code ret_code = (expr); \
+		if (ret_code != MANIFEST_SUCCESS) {          \
+			return ret_code;                     \
+		}                                            \
+	} while (0)
+
+#define VM_NAME_BUF_SIZE (2 + 5 + 1) /* "vm" + number + null terminator */
+static_assert(MAX_VMS <= 99999, "Insufficient VM_NAME_BUF_SIZE");
+
+/**
+ * Generates a string with the two letters "vm" followed by an integer.
+ * Assumes `buf` is of size VM_NAME_BUF_SIZE.
+ */
+static const char *generate_vm_node_name(char *buf, spci_vm_id_t vm_id)
+{
+	static const char *digits = "0123456789";
+	char *ptr = buf + VM_NAME_BUF_SIZE;
+
+	*(--ptr) = '\0';
+	do {
+		*(--ptr) = digits[vm_id % 10];
+		vm_id /= 10;
+	} while (vm_id);
+	*(--ptr) = 'm';
+	*(--ptr) = 'v';
+
+	return ptr;
+}
+
+static enum manifest_return_code read_string(const struct fdt_node *node,
+					     const char *property,
+					     struct memiter *out)
+{
+	const char *data;
+	uint32_t size;
+
+	if (!fdt_read_property(node, property, &data, &size)) {
+		return MANIFEST_ERROR_PROPERTY_NOT_FOUND;
+	}
+
+	if (data[size - 1] != '\0') {
+		return MANIFEST_ERROR_MALFORMED_STRING;
+	}
+
+	memiter_init(out, data, size - 1);
+	return MANIFEST_SUCCESS;
+}
+
+static enum manifest_return_code read_uint64(const struct fdt_node *node,
+					     const char *property,
+					     uint64_t *out)
+{
+	const char *data;
+	uint32_t size;
+
+	if (!fdt_read_property(node, property, &data, &size)) {
+		return MANIFEST_ERROR_PROPERTY_NOT_FOUND;
+	}
+
+	if (!fdt_parse_number(data, size, out)) {
+		return MANIFEST_ERROR_MALFORMED_INTEGER;
+	}
+
+	return MANIFEST_SUCCESS;
+}
+
+static enum manifest_return_code read_uint16(const struct fdt_node *node,
+					     const char *property,
+					     uint16_t *out)
+{
+	uint64_t value;
+
+	TRY(read_uint64(node, property, &value));
+
+	if (value > UINT16_MAX) {
+		return MANIFEST_ERROR_INTEGER_OVERFLOW;
+	}
+
+	*out = (uint16_t)value;
+	return MANIFEST_SUCCESS;
+}
+
+static enum manifest_return_code parse_vm(struct fdt_node *node,
+					  struct manifest_vm *vm,
+					  spci_vm_id_t vm_id)
+{
+	TRY(read_string(node, "debug_name", &vm->debug_name));
+	if (vm_id != HF_PRIMARY_VM_ID) {
+		TRY(read_string(node, "kernel_filename",
+				&vm->secondary.kernel_filename));
+		TRY(read_uint64(node, "mem_size", &vm->secondary.mem_size));
+		TRY(read_uint16(node, "vcpu_count", &vm->secondary.vcpu_count));
+	}
+	return MANIFEST_SUCCESS;
+}
+
+/**
+ * Parse manifest from FDT.
+ */
+enum manifest_return_code manifest_init(struct manifest *manifest,
+					struct memiter *fdt)
+{
+	char vm_name_buf[VM_NAME_BUF_SIZE];
+	struct fdt_node hyp_node;
+	size_t i = 0;
+	bool found_primary_vm = false;
+
+	memset_s(manifest, sizeof(*manifest), 0, sizeof(*manifest));
+
+	/* Find hypervisor node. */
+	if (!fdt_root_node(&hyp_node,
+			   (const struct fdt_header *)memiter_base(fdt))) {
+		return MANIFEST_ERROR_CORRUPTED_FDT;
+	}
+	if (!fdt_find_child(&hyp_node, "")) {
+		return MANIFEST_ERROR_NO_ROOT_FDT_NODE;
+	}
+	if (!fdt_find_child(&hyp_node, "hypervisor")) {
+		return MANIFEST_ERROR_NO_HYPERVISOR_FDT_NODE;
+	}
+
+	/* Iterate over reserved VM IDs and check no such nodes exist. */
+	for (i = 0; i < HF_VM_ID_OFFSET; i++) {
+		spci_vm_id_t vm_id = (spci_vm_id_t)i;
+		struct fdt_node vm_node = hyp_node;
+		const char *vm_name = generate_vm_node_name(vm_name_buf, vm_id);
+
+		if (fdt_find_child(&vm_node, vm_name)) {
+			return MANIFEST_ERROR_RESERVED_VM_ID;
+		}
+	}
+
+	/* Iterate over VM nodes until we find one that does not exist. */
+	for (i = 0; i <= MAX_VMS; ++i) {
+		spci_vm_id_t vm_id = HF_VM_ID_OFFSET + i;
+		struct fdt_node vm_node = hyp_node;
+		const char *vm_name = generate_vm_node_name(vm_name_buf, vm_id);
+
+		if (!fdt_find_child(&vm_node, vm_name)) {
+			break;
+		}
+
+		if (i == MAX_VMS) {
+			return MANIFEST_ERROR_TOO_MANY_VMS;
+		}
+
+		if (vm_id == HF_PRIMARY_VM_ID) {
+			CHECK(found_primary_vm == false); /* sanity check */
+			found_primary_vm = true;
+		}
+
+		manifest->num_vms = i + 1;
+		TRY(parse_vm(&vm_node, &manifest->vm[i], vm_id));
+	}
+
+	if (!found_primary_vm) {
+		return MANIFEST_ERROR_NO_PRIMARY_VM;
+	}
+
+	return MANIFEST_SUCCESS;
+}
+
+const char *manifest_strerror(enum manifest_return_code ret_code)
+{
+	switch (ret_code) {
+	case MANIFEST_SUCCESS:
+		return "Success";
+	case MANIFEST_ERROR_CORRUPTED_FDT:
+		return "Manifest failed FDT validation";
+	case MANIFEST_ERROR_NO_ROOT_FDT_NODE:
+		return "Could not find root node of manifest";
+	case MANIFEST_ERROR_NO_HYPERVISOR_FDT_NODE:
+		return "Could not find \"hypervisor\" node in manifest";
+	case MANIFEST_ERROR_RESERVED_VM_ID:
+		return "Manifest defines a VM with a reserved ID";
+	case MANIFEST_ERROR_NO_PRIMARY_VM:
+		return "Manifest does not contain a primary VM entry";
+	case MANIFEST_ERROR_TOO_MANY_VMS:
+		return "Manifest specifies more VMs than Hafnium has "
+		       "statically allocated space for";
+	case MANIFEST_ERROR_PROPERTY_NOT_FOUND:
+		return "Property not found";
+	case MANIFEST_ERROR_MALFORMED_STRING:
+		return "Malformed string property";
+	case MANIFEST_ERROR_MALFORMED_INTEGER:
+		return "Malformed integer property";
+	case MANIFEST_ERROR_INTEGER_OVERFLOW:
+		return "Integer overflow";
+	}
+
+	panic("Unexpected manifest return code.");
+}
diff --git a/src/manifest_test.cc b/src/manifest_test.cc
new file mode 100644
index 0000000..3a86a7a
--- /dev/null
+++ b/src/manifest_test.cc
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2019 The Hafnium Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gmock/gmock.h>
+
+extern "C" {
+#include "hf/manifest.h"
+}
+
+namespace
+{
+using ::testing::Eq;
+using ::testing::NotNull;
+
+/*
+ * DTB files compiled with:
+ *   $ dtc -I dts -O dtb --out-version 17 test.dts | xxd -i
+ */
+
+/*
+ * /dts-v1/;
+ *
+ * / {
+ * };
+ *
+ */
+constexpr uint8_t dtb_empty_root[] = {
+	0xd0, 0x0d, 0xfe, 0xed, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x38,
+	0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x11,
+	0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09};
+
+TEST(manifest, empty_root)
+{
+	struct manifest m;
+	struct memiter it;
+
+	memiter_init(&it, dtb_empty_root, sizeof(dtb_empty_root));
+	ASSERT_EQ(manifest_init(&m, &it),
+		  MANIFEST_ERROR_NO_HYPERVISOR_FDT_NODE);
+}
+
+/*
+ * /dts-v1/;
+ *
+ * / {
+ * 	hypervisor {
+ * 	};
+ * };
+ *
+ */
+constexpr uint8_t dtb_no_vms[] = {
+	0xd0, 0x0d, 0xfe, 0xed, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x38,
+	0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x11,
+	0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x68, 0x79, 0x70, 0x65,
+	0x72, 0x76, 0x69, 0x73, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
+	0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09};
+
+TEST(manifest, no_vms)
+{
+	struct manifest m;
+	struct memiter it;
+
+	memiter_init(&it, dtb_no_vms, sizeof(dtb_no_vms));
+	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_ERROR_NO_PRIMARY_VM);
+}
+
+/*
+ * /dts-v1/;
+ *
+ * / {
+ * 	hypervisor {
+ * 		vm1 {
+ * 			debug_name = "primary_vm";
+ * 		};
+ * 		vm0 {
+ * 			debug_name = "reserved_vm";
+ * 			vcpu_count = <1>;
+ * 			mem_size = <4096>;
+ * 			kernel_filename = "kernel";
+ * 		};
+ * 	};
+ * };
+ *
+ */
+constexpr uint8_t dtb_reserved_vmid[] = {
+	0xd0, 0x0d, 0xfe, 0xed, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0x00, 0x38,
+	0x00, 0x00, 0x00, 0xd8, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x11,
+	0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2f,
+	0x00, 0x00, 0x00, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x68, 0x79, 0x70, 0x65,
+	0x72, 0x76, 0x69, 0x73, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	0x76, 0x6d, 0x31, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0b,
+	0x00, 0x00, 0x00, 0x00, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f,
+	0x76, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01,
+	0x76, 0x6d, 0x30, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0c,
+	0x00, 0x00, 0x00, 0x00, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x64,
+	0x5f, 0x76, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
+	0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03,
+	0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x10, 0x00,
+	0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1f,
+	0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
+	0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09,
+	0x64, 0x65, 0x62, 0x75, 0x67, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x00, 0x76,
+	0x63, 0x70, 0x75, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x00, 0x6d, 0x65,
+	0x6d, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x00, 0x6b, 0x65, 0x72, 0x6e, 0x65,
+	0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x00};
+
+TEST(manifest, reserved_vmid)
+{
+	struct manifest m;
+	struct memiter it;
+
+	memiter_init(&it, dtb_reserved_vmid, sizeof(dtb_reserved_vmid));
+	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_ERROR_RESERVED_VM_ID);
+}
+
+/*
+ * /dts-v1/;
+ *
+ * / {
+ * 	hypervisor {
+ * 		vm1 {
+ * 			debug_name = "";
+ * 		};
+ * 		vm2 {
+ * 			debug_name = "";
+ * 			vcpu_count = <65535>;
+ * 			mem_size = <0>;
+ * 			kernel_filename = "";
+ * 		};
+ * 	};
+ * };
+ *
+ */
+constexpr uint8_t dtb_last_valid_vcpu_count[] = {
+	0xd0, 0x0d, 0xfe, 0xed, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, 0x38,
+	0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x11,
+	0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2f,
+	0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x68, 0x79, 0x70, 0x65,
+	0x72, 0x76, 0x69, 0x73, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	0x76, 0x6d, 0x31, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
+	0x00, 0x00, 0x00, 0x01, 0x76, 0x6d, 0x32, 0x00, 0x00, 0x00, 0x00, 0x03,
+	0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0b,
+	0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
+	0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
+	0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02,
+	0x00, 0x00, 0x00, 0x09, 0x64, 0x65, 0x62, 0x75, 0x67, 0x5f, 0x6e, 0x61,
+	0x6d, 0x65, 0x00, 0x76, 0x63, 0x70, 0x75, 0x5f, 0x63, 0x6f, 0x75, 0x6e,
+	0x74, 0x00, 0x6d, 0x65, 0x6d, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x00, 0x6b,
+	0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61,
+	0x6d, 0x65, 0x00};
+
+/* Same as above, set "vcpu_count" to 65536. */
+constexpr uint8_t dtb_first_invalid_vcpu_count[] = {
+	0xd0, 0x0d, 0xfe, 0xed, 0x00, 0x00, 0x00, 0xf3, 0x00, 0x00, 0x00, 0x38,
+	0x00, 0x00, 0x00, 0xc4, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x11,
+	0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2f,
+	0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x68, 0x79, 0x70, 0x65,
+	0x72, 0x76, 0x69, 0x73, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	0x76, 0x6d, 0x31, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
+	0x00, 0x00, 0x00, 0x01, 0x76, 0x6d, 0x32, 0x00, 0x00, 0x00, 0x00, 0x03,
+	0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0b,
+	0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
+	0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
+	0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02,
+	0x00, 0x00, 0x00, 0x09, 0x64, 0x65, 0x62, 0x75, 0x67, 0x5f, 0x6e, 0x61,
+	0x6d, 0x65, 0x00, 0x76, 0x63, 0x70, 0x75, 0x5f, 0x63, 0x6f, 0x75, 0x6e,
+	0x74, 0x00, 0x6d, 0x65, 0x6d, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x00, 0x6b,
+	0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61,
+	0x6d, 0x65, 0x00};
+
+TEST(manifest, vcpu_count_limit)
+{
+	struct manifest m;
+	struct memiter it;
+
+	memiter_init(&it, dtb_last_valid_vcpu_count,
+		     sizeof(dtb_last_valid_vcpu_count));
+	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_SUCCESS);
+	ASSERT_EQ(m.num_vms, 2);
+	ASSERT_EQ(m.vm[1].secondary.vcpu_count, UINT16_MAX);
+
+	memiter_init(&it, dtb_first_invalid_vcpu_count,
+		     sizeof(dtb_first_invalid_vcpu_count));
+	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_ERROR_INTEGER_OVERFLOW);
+}
+
+/*
+ * /dts-v1/;
+ *
+ * / {
+ * 	hypervisor {
+ * 		vm1 {
+ * 			debug_name = "primary_vm";
+ * 		};
+ * 		vm3 {
+ * 			debug_name = "second_secondary_vm";
+ * 			vcpu_count = <43>;
+ * 			mem_size = <0x12345>;
+ * 			kernel_filename = "second_kernel";
+ * 		};
+ * 		vm2 {
+ * 			debug_name = "first_secondary_vm";
+ * 			vcpu_count = <42>;
+ * 			mem_size = <12345>;
+ * 			kernel_filename = "first_kernel";
+ * 		};
+ * 	};
+ * };
+ *
+ */
+constexpr uint8_t dtb_valid[] = {
+	0xd0, 0x0d, 0xfe, 0xed, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x00, 0x38,
+	0x00, 0x00, 0x01, 0x50, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x11,
+	0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2f,
+	0x00, 0x00, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x68, 0x79, 0x70, 0x65,
+	0x72, 0x76, 0x69, 0x73, 0x6f, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	0x76, 0x6d, 0x31, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0b,
+	0x00, 0x00, 0x00, 0x00, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f,
+	0x76, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01,
+	0x76, 0x6d, 0x33, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x14,
+	0x00, 0x00, 0x00, 0x00, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x5f, 0x73,
+	0x65, 0x63, 0x6f, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x5f, 0x76, 0x6d, 0x00,
+	0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0b,
+	0x00, 0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04,
+	0x00, 0x00, 0x00, 0x16, 0x00, 0x01, 0x23, 0x45, 0x00, 0x00, 0x00, 0x03,
+	0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x1f, 0x73, 0x65, 0x63, 0x6f,
+	0x6e, 0x64, 0x5f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x76, 0x6d, 0x32, 0x00,
+	0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00,
+	0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64,
+	0x61, 0x72, 0x79, 0x5f, 0x76, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
+	0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x2a,
+	0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x16,
+	0x00, 0x00, 0x30, 0x39, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0d,
+	0x00, 0x00, 0x00, 0x1f, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x6b, 0x65,
+	0x72, 0x6e, 0x65, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
+	0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09,
+	0x64, 0x65, 0x62, 0x75, 0x67, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x00, 0x76,
+	0x63, 0x70, 0x75, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x00, 0x6d, 0x65,
+	0x6d, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x00, 0x6b, 0x65, 0x72, 0x6e, 0x65,
+	0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x00};
+
+TEST(manifest, valid)
+{
+	struct manifest m;
+	struct manifest_vm *vm;
+	struct memiter it;
+
+	memiter_init(&it, dtb_valid, sizeof(dtb_valid));
+
+	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_SUCCESS);
+	ASSERT_EQ(m.num_vms, 3);
+
+	vm = &m.vm[0];
+	ASSERT_TRUE(memiter_iseq(&vm->debug_name, "primary_vm"));
+
+	vm = &m.vm[1];
+	ASSERT_TRUE(memiter_iseq(&vm->debug_name, "first_secondary_vm"));
+	ASSERT_EQ(vm->secondary.vcpu_count, 42);
+	ASSERT_EQ(vm->secondary.mem_size, 12345);
+	ASSERT_TRUE(
+		memiter_iseq(&vm->secondary.kernel_filename, "first_kernel"));
+
+	vm = &m.vm[2];
+	ASSERT_TRUE(memiter_iseq(&vm->debug_name, "second_secondary_vm"));
+	ASSERT_EQ(vm->secondary.vcpu_count, 43);
+	ASSERT_EQ(vm->secondary.mem_size, 0x12345);
+	ASSERT_TRUE(
+		memiter_iseq(&vm->secondary.kernel_filename, "second_kernel"));
+}
+
+} /* namespace */
diff --git a/src/memiter.c b/src/memiter.c
index 5ff54ee..3dec9c6 100644
--- a/src/memiter.c
+++ b/src/memiter.c
@@ -16,6 +16,7 @@
 
 #include "hf/memiter.h"
 
+#include "hf/dlog.h"
 #include "hf/std.h"
 
 /**
@@ -93,6 +94,19 @@
 }
 
 /**
+ * Prints the contents of memory covered by the iterator to dlog. It does *not*
+ * assume that the string is null-terminated.
+ */
+void memiter_dlog_str(struct memiter *it)
+{
+	const char *p;
+
+	for (p = it->next; p < it->limit; ++p) {
+		dlog("%c", *p);
+	}
+}
+
+/**
  * Parses the next string that represents a 64-bit number.
  */
 bool memiter_parse_uint(struct memiter *it, uint64_t *value)
@@ -138,3 +152,16 @@
 
 	return true;
 }
+
+const void *memiter_base(struct memiter *it)
+{
+	return (const void *)it->next;
+}
+
+/**
+ * Returns the number of bytes in interval [it.next, it.limit).
+ */
+size_t memiter_size(struct memiter *it)
+{
+	return it->limit - it->next;
+}
diff --git a/test/linux/BUILD.gn b/test/linux/BUILD.gn
index 84e72bc..fcc5907 100644
--- a/test/linux/BUILD.gn
+++ b/test/linux/BUILD.gn
@@ -57,11 +57,10 @@
 initrd("linux_test") {
   testonly = true
 
+  manifest = "manifest.dts"
   primary_vm = "//third_party:linux__prebuilt"
   primary_initrd = ":linux_test_initrd"
   secondary_vms = [ [
-        "1048576",
-        "1",
         "socket0",
         ":socket_vm0",
       ] ]
diff --git a/test/linux/manifest.dts b/test/linux/manifest.dts
new file mode 100644
index 0000000..5437968
--- /dev/null
+++ b/test/linux/manifest.dts
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 The Hafnium Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/dts-v1/;
+
+/ {
+	hypervisor {
+		vm1 {
+			debug_name = "primary";
+		};
+
+		vm2 {
+			debug_name = "socket0";
+			vcpu_count = <1>;
+			mem_size = <0x100000>;
+			kernel_filename = "socket0";
+		};
+	};
+};
diff --git a/test/vmapi/gicv3/BUILD.gn b/test/vmapi/gicv3/BUILD.gn
index 162a84a..196f169 100644
--- a/test/vmapi/gicv3/BUILD.gn
+++ b/test/vmapi/gicv3/BUILD.gn
@@ -41,10 +41,9 @@
 initrd("gicv3_test") {
   testonly = true
 
+  manifest = "manifest.dts"
   primary_vm = ":gicv3_test_vm"
   secondary_vms = [ [
-        "1048576",
-        "1",
         "services0",
         "services:gicv3_service_vm0",
       ] ]
diff --git a/test/vmapi/gicv3/manifest.dts b/test/vmapi/gicv3/manifest.dts
new file mode 100644
index 0000000..4e90046
--- /dev/null
+++ b/test/vmapi/gicv3/manifest.dts
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 The Hafnium Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/dts-v1/;
+
+/ {
+	hypervisor {
+		vm1 {
+			debug_name = "primary";
+		};
+
+		vm2 {
+			debug_name = "services0";
+			vcpu_count = <1>;
+			mem_size = <0x100000>;
+			kernel_filename = "services0";
+		};
+	};
+};
diff --git a/test/vmapi/primary_only/BUILD.gn b/test/vmapi/primary_only/BUILD.gn
index cad9077..ecb197a 100644
--- a/test/vmapi/primary_only/BUILD.gn
+++ b/test/vmapi/primary_only/BUILD.gn
@@ -30,6 +30,6 @@
 
 initrd("primary_only_test") {
   testonly = true
-
+  manifest = "manifest.dts"
   primary_vm = ":primary_only_test_vm"
 }
diff --git a/test/vmapi/primary_only/manifest.dts b/test/vmapi/primary_only/manifest.dts
new file mode 100644
index 0000000..0e1b082
--- /dev/null
+++ b/test/vmapi/primary_only/manifest.dts
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2019 The Hafnium Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/dts-v1/;
+
+/ {
+	hypervisor {
+		vm1 {
+			debug_name = "primary";
+		};
+	};
+};
diff --git a/test/vmapi/primary_with_secondaries/BUILD.gn b/test/vmapi/primary_with_secondaries/BUILD.gn
index d24570f..60b78a0 100644
--- a/test/vmapi/primary_with_secondaries/BUILD.gn
+++ b/test/vmapi/primary_with_secondaries/BUILD.gn
@@ -47,23 +47,19 @@
 initrd("primary_with_secondaries_test") {
   testonly = true
 
+  manifest = "manifest.dts"
+
   primary_vm = ":primary_with_secondaries_test_vm"
   secondary_vms = [
     [
-      "1048576",
-      "1",
       "services0",
       "services:service_vm0",
     ],
     [
-      "1048576",
-      "1",
       "services1",
       "services:service_vm1",
     ],
     [
-      "1048576",
-      "2",
       "services2",
       "services:service_vm2",
     ],
diff --git a/test/vmapi/primary_with_secondaries/manifest.dts b/test/vmapi/primary_with_secondaries/manifest.dts
new file mode 100644
index 0000000..741becd
--- /dev/null
+++ b/test/vmapi/primary_with_secondaries/manifest.dts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 The Hafnium Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/dts-v1/;
+
+/ {
+	hypervisor {
+		vm1 {
+			debug_name = "primary";
+		};
+
+		vm2 {
+			debug_name = "services0";
+			vcpu_count = <1>;
+			mem_size = <0x100000>;
+			kernel_filename = "services0";
+		};
+
+		vm3 {
+			debug_name = "services1";
+			vcpu_count = <1>;
+			mem_size = <0x100000>;
+			kernel_filename = "services1";
+		};
+
+		vm4 {
+			debug_name = "services2";
+			vcpu_count = <2>;
+			mem_size = <0x100000>;
+			kernel_filename = "services2";
+		};
+	};
+};