Merge manifest into the board DT, add boot flow drivers

Hafnium currently supports two kinds of boot flow:
(a) Linux-like, where the board FDT is passed by the boot loader to
the OS kernel in first kernel arg (this is used by QEMU, FVP, RPi, ...),
(b) Android-like, where the FDT used by Hafnium is compiled into it and
the VMs have their own.

This used to be implemented using weak symbols and each board spec
overriding it with its own implementation. This patch introduces the
concept of boot-flow drivers in the main tree, and the corresponding
driver selected using a config option in board spec. Drivers for the two
boot-flows described above are implemented.

Simultaneously, the manifest is now read from the FDT available at
Hafnium init time. This required two notable changes:
(1) all values are copied into the manifest struct because FDT is
unmapped, modified and passed to the primary VM,
(2) manifest is now written as an overlay; QEMU and FVP test drivers
overlay it automatically.

Bug: 117551352
Change-Id: Ieae7fe4ef5b3047174ec0ad057e487660ccd5a03
diff --git a/Makefile b/Makefile
index 3473b2e..c000c72 100644
--- a/Makefile
+++ b/Makefile
@@ -72,10 +72,10 @@
 .PHONY: format
 format:
 	@echo "Formatting..."
-	@find src/ -name \*.c -o -name \*.cc -o -name \*.h | xargs clang-format -style file -i
-	@find inc/ -name \*.c -o -name \*.cc -o -name \*.h | xargs clang-format -style file -i
-	@find test/ -name \*.c -o -name \*.cc -o -name \*.h | xargs clang-format -style file -i
-	@find project/ -name \*.c -o -name \*.cc -o -name \*.h | xargs clang-format -style file -i
+	@find src/ -name \*.c -o -name \*.cc -o -name \*.h | xargs -r clang-format -style file -i
+	@find inc/ -name \*.c -o -name \*.cc -o -name \*.h | xargs -r clang-format -style file -i
+	@find test/ -name \*.c -o -name \*.cc -o -name \*.h | xargs -r clang-format -style file -i
+	@find project/ -name \*.c -o -name \*.cc -o -name \*.h | xargs -r clang-format -style file -i
 	@find . \( -name \*.gn -o -name \*.gni \) | xargs -n1 $(GN) format
 
 .PHONY: checkpatch
diff --git a/build/image/dtc.py b/build/image/dtc.py
index 02314f2..2ae8efe 100755
--- a/build/image/dtc.py
+++ b/build/image/dtc.py
@@ -22,26 +22,53 @@
 import sys
 
 HF_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
-DTC = os.path.join(HF_ROOT, "prebuilts", "linux-x64", "dtc", "dtc")
+DTC_ROOT = os.path.join(HF_ROOT, "prebuilts", "linux-x64", "dtc")
+DTC = os.path.join(DTC_ROOT, "dtc")
+FDTOVERLAY = os.path.join(DTC_ROOT, "fdtoverlay")
 
-def main():
-    parser = argparse.ArgumentParser()
-    parser.add_argument("-i", "--input-file")
-    parser.add_argument("-o", "--output-file")
-    args = parser.parse_args()
-
-    dtc_args = [
+def cmd_compile(args):
+    exec_args = [
             DTC,
             "-I", "dts", "-O", "dtb",
             "--out-version", "17",
         ]
 
     if args.output_file:
-        dtc_args += [ "-o", args.output_file ]
+        exec_args += [ "-o", args.output_file ]
     if args.input_file:
-        dtc_args += [ args.input_file ]
+        exec_args += [ args.input_file ]
 
-    return subprocess.call(dtc_args)
+    return subprocess.call(exec_args)
+
+def cmd_overlay(args):
+    exec_args = [
+            FDTOVERLAY,
+            "-i", args.base_dtb,
+            "-o", args.output_dtb,
+        ] + args.overlay_dtb
+    return subprocess.call(exec_args)
+
+def main():
+    parser = argparse.ArgumentParser()
+    subparsers = parser.add_subparsers(dest="command")
+
+    parser_compile = subparsers.add_parser("compile", help="compile DTS to DTB")
+    parser_compile.add_argument("-i", "--input-file")
+    parser_compile.add_argument("-o", "--output-file")
+
+    parser_overlay = subparsers.add_parser("overlay", help="merge DTBs")
+    parser_overlay.add_argument("output_dtb")
+    parser_overlay.add_argument("base_dtb")
+    parser_overlay.add_argument("overlay_dtb", nargs='*')
+
+    args = parser.parse_args()
+
+    if args.command == "compile":
+        return cmd_compile(args)
+    elif args.command == "overlay":
+        return cmd_overlay(args)
+    else:
+        raise Error("Unknown command: {}".format(args.command))
 
 if __name__ == "__main__":
     sys.exit(main())
diff --git a/build/image/generate_initrd.py b/build/image/generate_initrd.py
index 8342cfb..38ce19c 100644
--- a/build/image/generate_initrd.py
+++ b/build/image/generate_initrd.py
@@ -29,7 +29,6 @@
 
 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(
@@ -46,10 +45,6 @@
     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.
diff --git a/build/image/image.gni b/build/image/image.gni
index 6c8ea17..6865f87 100644
--- a/build/image/image.gni
+++ b/build/image/image.gni
@@ -158,16 +158,15 @@
                            ])
     script = "//build/image/dtc.py"
 
-    dtb_file = "${target_out_dir}/{{source_name_part}}.dtb"
-
     outputs = [
-      dtb_file,
+      invoker.output_pattern,
     ]
     args = [
+      "compile",
       "-i",
       "{{source}}",
       "-o",
-      rebase_path(dtb_file),
+      rebase_path(invoker.output_pattern),
     ]
   }
 }
@@ -178,26 +177,29 @@
          "initrd() must specify a \"primary_vm\" value")
 
   manifest_target = "${target_name}__manifest"
+  base_out_dir = "${target_out_dir}/${target_name}"
 
+  # Generate manifest.dtbo
   device_tree(manifest_target) {
     sources = [
       invoker.manifest,
     ]
+    output_pattern = "${base_out_dir}/{{source_name_part}}.dtbo"
   }
 
   action(target_name) {
     forward_variables_from(invoker, [ "testonly" ])
     script = "//build/image/generate_initrd.py"
 
-    initrd_base = "${target_out_dir}/${target_name}/initrd"
-    initrd_file = "${initrd_base}.img"
-    initrd_staging = "${initrd_base}"
+    initrd_file = "${base_out_dir}/initrd.img"
+    initrd_staging = "${base_out_dir}/initrd"
 
     # Cannot get target outputs here as they are defined in a different file.
     primary_vm_image = get_label_info(invoker.primary_vm, "target_out_dir") +
                        "/" + get_label_info(invoker.primary_vm, "name") + ".bin"
 
     deps = [
+      ":${manifest_target}",
       invoker.primary_vm,
     ]
     args = [
@@ -235,13 +237,6 @@
       }
     }
 
-    manifest_target_outputs = get_target_outputs(":${manifest_target}")
-    deps += [ ":${manifest_target}" ]
-    args += [
-      "--manifest",
-      rebase_path(manifest_target_outputs[0]),
-    ]
-
     outputs = [
       initrd_file,
     ]
diff --git a/build/toolchain/BUILD.gn b/build/toolchain/BUILD.gn
index f7caa6c..c152b39 100644
--- a/build/toolchain/BUILD.gn
+++ b/build/toolchain/BUILD.gn
@@ -39,6 +39,7 @@
   toolchain_args = {
     use_platform = true
     plat_arch = "fake"
+    plat_boot_flow = "//src/arch/fake:boot_flow"
     plat_console = "//src/arch/fake:console"
   }
 }
diff --git a/build/toolchain/embedded.gni b/build/toolchain/embedded.gni
index a1eaeed..6785701 100644
--- a/build/toolchain/embedded.gni
+++ b/build/toolchain/embedded.gni
@@ -320,6 +320,7 @@
     }
 
     toolchain_args = {
+      plat_boot_flow = invoker.boot_flow
       plat_console = invoker.console
       forward_variables_from(invoker.toolchain_args, "*")
     }
@@ -340,6 +341,7 @@
     forward_variables_from(invoker,
                            [
                              "origin_address",
+                             "boot_flow",
                              "console",
                              "gic_version",
                              "gicd_base_address",
@@ -367,6 +369,7 @@
                              "toolchain_args",
                            ])
     cpu = "${invoker.cpu}+fp"
+    boot_flow = "//src/arch/fake:boot_flow"
     console = "//src/arch/aarch64/hftest:console"
 
     # Nonsense values because they are required but shouldn't be used.
diff --git a/build/toolchain/host.gni b/build/toolchain/host.gni
index 5b646b6..4d2dfa3 100644
--- a/build/toolchain/host.gni
+++ b/build/toolchain/host.gni
@@ -150,6 +150,7 @@
         # When building for the ${target_name}, use the fake architecture to make things
         # testable.
         plat_arch = "fake"
+        plat_boot_flow = "//src/arch/fake:boot_flow"
         plat_console = "//src/arch/fake:console"
         plat_heap_pages = invoker.heap_pages
         plat_max_cpus = invoker.max_cpus
diff --git a/build/toolchain/platform.gni b/build/toolchain/platform.gni
index 4e30a1b..b99ca64 100644
--- a/build/toolchain/platform.gni
+++ b/build/toolchain/platform.gni
@@ -19,6 +19,11 @@
 
   # The architecture of the platform.
   plat_arch = ""
+
+  # Boot flow driver to be used by the platform, specified as build target.
+  plat_boot_flow = ""
+
+  # Console driver to be used for the platform, specified as build target.
   plat_console = ""
 
   # The number of pages to allocate for the hypervisor heap.
diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md
index dba6365..1989855 100644
--- a/docs/GettingStarted.md
+++ b/docs/GettingStarted.md
@@ -51,11 +51,32 @@
 machines to run. Follow the [Hafnium RAM disk](HafniumRamDisk.md) instructions
 to create an initial RAM disk for Hafnium with Linux as the primary VM.
 
+Next, you need to create a manifest which will describe the VM to Hafnium.
+Follow the [Manifest](Manifest.md) instructions and build a DTBO with:
+```
+/dts-v1/;
+/plugin/;
+
+&{/} {
+	hypervisor {
+		vm1 {
+			debug_name = "Linux VM";
+		};
+	};
+};
+```
+
+Dump the DTB used by QEMU:
+```shell
+qemu-system-aarch64 -M virt,gic_version=3 -cpu cortex-a57 -nographic -machine virtualization=true -kernel out/reference/qemu_aarch64_clang/hafnium.bin -initrd initrd.img -append "rdinit=/sbin/init" -machine dumpdtb=qemu.dtb
+```
+and follow instructions in [Manifest](Manifest.md) to overlay it with the manifest.
+
 The following command line will run Hafnium, with the RAM disk just created,
 which will then boot into the primary Linux VM:
 
 ```shell
-qemu-system-aarch64 -M virt,gic_version=3 -cpu cortex-a57 -nographic -machine virtualization=true -kernel out/reference/qemu_aarch64_clang/hafnium.bin -initrd initrd.img -append "rdinit=/sbin/init"
+qemu-system-aarch64 -M virt,gic_version=3 -cpu cortex-a57 -nographic -machine virtualization=true -kernel out/reference/qemu_aarch64_clang/hafnium.bin -initrd initrd.img -append "rdinit=/sbin/init" -dtb=qemu_with_manifest.dtb
 ```
 
 ## Running tests
diff --git a/docs/HafniumRamDisk.md b/docs/HafniumRamDisk.md
index 37e27db..c09568d 100644
--- a/docs/HafniumRamDisk.md
+++ b/docs/HafniumRamDisk.md
@@ -5,71 +5,11 @@
 
 *   `vmlinuz` -- the kernel of the primary VM.
 *   `initrd.img` -- the initial ramdisk of the primary VM.
-*   `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.
 
-## Manifest file
-
-The format is currently a simple Device Tree:
-
-```
-/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
-dtc -I dts -O dtb --out-version 17 -o manifest.dtb <manifest_source_file>
-```
-
 ## Create a RAM disk for Hafnium
 
 Assuming that a subdirectory called `initrd` contains the files listed in the
diff --git a/docs/Manifest.md b/docs/Manifest.md
new file mode 100644
index 0000000..58a5600
--- /dev/null
+++ b/docs/Manifest.md
@@ -0,0 +1,79 @@
+# Hafnium Manifest
+
+## Format
+
+The format of the manifest is a simple DeviceTree overlay:
+
+```
+/dts-v1/;
+/plugin/;
+
+&{/} {
+	hypervisor {
+		vm1 {
+			debug_name = "name";
+		};
+
+		vm2 {
+			debug_name = "name";
+			kernel_filename = "filename";
+			vcpu_count = <N>;
+			mem_size = <M>;
+		};
+		...
+	};
+};
+```
+
+Note: `&{/}` is a syntactic sugar expanded by the DTC compiler. Make sure to
+use the DTC in `prebuilts/` as the version packaged with your OS may not support
+it yet.
+
+## Example
+
+The following manifest defines two secondary VMs, the first one with 1MB of
+memory, 2 CPUs and kernel image called `kernel0` (matches filename in Hafnium's
+[ramdisk](HafniumRamDisk.md)), while the second one has 2MB of memory, 4 CPUs
+and a kernel image called `kernel1`.
+
+```
+/dts-v1/;
+/plugin/;
+
+&{/} {
+	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>;
+		};
+	};
+};
+```
+
+## Compiling
+
+Hafnium expects the manifest as part of the board FDT, i.e. DeviceTree in binary
+format (DTB).
+
+First, compile the manifest into a DTBO (binary overlay):
+```shell
+prebuilts/linux-x64/dtc/dtc -I dts -O dtb --out-version 17 -o manifest.dtbo <manifest_source_file>
+```
+
+Then overlay it with the DTB of your board:
+```shell
+prebuilts/linux-x64/dtc/fdtoverlay -i <board DTB> -o <output DTB> manifest.dtbo
+```
diff --git a/inc/hf/boot_flow.h b/inc/hf/boot_flow.h
new file mode 100644
index 0000000..e96ad1b
--- /dev/null
+++ b/inc/hf/boot_flow.h
@@ -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.
+ */
+
+#pragma once
+
+#include "hf/boot_params.h"
+#include "hf/manifest.h"
+#include "hf/mm.h"
+
+bool boot_flow_init(struct mm_stage1_locked stage1_locked,
+		    struct manifest *manifest, struct boot_params *boot_params,
+		    struct mpool *ppool);
diff --git a/inc/hf/boot_params.h b/inc/hf/boot_params.h
index ee46777..59b62d5 100644
--- a/inc/hf/boot_params.h
+++ b/inc/hf/boot_params.h
@@ -20,6 +20,7 @@
 
 #include "hf/arch/cpu.h"
 
+#include "hf/fdt.h"
 #include "hf/mm.h"
 #include "hf/mpool.h"
 
@@ -47,7 +48,6 @@
 	paddr_t initrd_end;
 };
 
-bool plat_get_boot_params(struct mm_stage1_locked stage1_locked,
-			  struct boot_params *p, struct mpool *ppool);
-bool plat_update_boot_params(struct mm_stage1_locked stage1_locked,
-			     struct boot_params_update *p, struct mpool *ppool);
+bool boot_params_init(struct boot_params *p, const struct fdt_node *fdt_root);
+bool boot_params_patch_fdt(struct mm_stage1_locked stage1_locked,
+			   struct boot_params_update *p, struct mpool *ppool);
diff --git a/inc/hf/fdt_handler.h b/inc/hf/fdt_handler.h
index 49d242e..70521b5 100644
--- a/inc/hf/fdt_handler.h
+++ b/inc/hf/fdt_handler.h
@@ -26,10 +26,10 @@
 			   struct mpool *ppool);
 bool fdt_unmap(struct mm_stage1_locked stage1_locked, struct fdt_header *fdt,
 	       struct mpool *ppool);
-void fdt_find_cpus(const struct fdt_node *root, cpu_id_t *cpu_ids,
+bool fdt_find_cpus(const struct fdt_node *root, cpu_id_t *cpu_ids,
 		   size_t *cpu_count);
-void fdt_find_memory_ranges(const struct fdt_node *root, struct boot_params *p);
-bool fdt_find_initrd(struct fdt_node *n, paddr_t *begin, paddr_t *end);
+bool fdt_find_memory_ranges(const struct fdt_node *root, struct boot_params *p);
+bool fdt_find_initrd(const struct fdt_node *root, paddr_t *begin, paddr_t *end);
 
 /** Apply an update to the FDT. */
 bool fdt_patch(struct mm_stage1_locked stage1_locked, paddr_t fdt_addr,
diff --git a/inc/hf/load.h b/inc/hf/load.h
index 2d9394a..d7ccd71 100644
--- a/inc/hf/load.h
+++ b/inc/hf/load.h
@@ -21,6 +21,7 @@
 
 #include "hf/boot_params.h"
 #include "hf/cpio.h"
+#include "hf/manifest.h"
 #include "hf/memiter.h"
 #include "hf/mm.h"
 #include "hf/mpool.h"
@@ -29,6 +30,6 @@
 		  const struct memiter *cpio, uintreg_t kernel_arg,
 		  struct memiter *initrd, struct mpool *ppool);
 bool load_secondary(struct mm_stage1_locked stage1_locked,
-		    const struct memiter *cpio,
+		    const struct manifest *manifest, const struct memiter *cpio,
 		    const struct boot_params *params,
 		    struct boot_params_update *update, struct mpool *ppool);
diff --git a/inc/hf/manifest.h b/inc/hf/manifest.h
index dd7a122..46bd205 100644
--- a/inc/hf/manifest.h
+++ b/inc/hf/manifest.h
@@ -16,19 +16,25 @@
 
 #pragma once
 
+#include "hf/fdt.h"
 #include "hf/memiter.h"
 #include "hf/spci.h"
 
 /**
+ * Maximum length of a string parsed from the FDT, including NULL terminator.
+ */
+#define MANIFEST_MAX_STRING_LENGTH 32
+
+/**
  * 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;
+	char debug_name[MANIFEST_MAX_STRING_LENGTH];
 
 	/* Properties specific to secondary VMs. */
 	struct {
-		struct memiter kernel_filename;
+		char kernel_filename[MANIFEST_MAX_STRING_LENGTH];
 		uint64_t mem_size;
 		spci_vcpu_count_t vcpu_count;
 	} secondary;
@@ -44,8 +50,6 @@
 
 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_NOT_COMPATIBLE,
 	MANIFEST_ERROR_RESERVED_VM_ID,
@@ -53,12 +57,13 @@
 	MANIFEST_ERROR_TOO_MANY_VMS,
 	MANIFEST_ERROR_PROPERTY_NOT_FOUND,
 	MANIFEST_ERROR_MALFORMED_STRING,
+	MANIFEST_ERROR_STRING_TOO_LONG,
 	MANIFEST_ERROR_MALFORMED_STRING_LIST,
 	MANIFEST_ERROR_MALFORMED_INTEGER,
 	MANIFEST_ERROR_INTEGER_OVERFLOW,
 };
 
 enum manifest_return_code manifest_init(struct manifest *manifest,
-					struct memiter *fdt);
+					const struct fdt_node *fdt_root);
 
 const char *manifest_strerror(enum manifest_return_code ret_code);
diff --git a/inc/hf/memiter.h b/inc/hf/memiter.h
index f8e8c25..e3f0db8 100644
--- a/inc/hf/memiter.h
+++ b/inc/hf/memiter.h
@@ -29,7 +29,6 @@
 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(const struct memiter *it);
diff --git a/inc/hf/plat/boot_flow.h b/inc/hf/plat/boot_flow.h
new file mode 100644
index 0000000..daaf5a6
--- /dev/null
+++ b/inc/hf/plat/boot_flow.h
@@ -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.
+ */
+
+#pragma once
+
+#include "hf/addr.h"
+#include "hf/fdt.h"
+
+paddr_t plat_get_fdt_addr(void);
+uintreg_t plat_get_kernel_arg(void);
+bool plat_get_initrd_range(const struct fdt_node *fdt_root, paddr_t *begin,
+			   paddr_t *end);
diff --git a/project/reference b/project/reference
index 105d519..653261a 160000
--- a/project/reference
+++ b/project/reference
@@ -1 +1 @@
-Subproject commit 105d51940c5068f7f07ccecd96885b4572589396
+Subproject commit 653261a75f4ef3f5a5335660b2d59df23b88d600
diff --git a/src/BUILD.gn b/src/BUILD.gn
index f6f55dc..e87ac1b 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -27,14 +27,15 @@
 # src targets will merge!
 source_set("src_not_testable_yet") {
   sources = [
+    "boot_params.c",
     "cpio.c",
     "load.c",
     "main.c",
-    "plat.c",
   ]
   deps = [
     ":src_testable",
     "//project/${project}/${plat_name}",
+    plat_boot_flow,
     plat_console,
   ]
 }
@@ -61,6 +62,7 @@
     ":mm",
     ":std",
     "//src/arch/${plat_arch}/hypervisor",
+    plat_boot_flow,
     plat_console,
   ]
 
diff --git a/src/arch/aarch64/boot_flow/BUILD.gn b/src/arch/aarch64/boot_flow/BUILD.gn
new file mode 100644
index 0000000..d709a9e
--- /dev/null
+++ b/src/arch/aarch64/boot_flow/BUILD.gn
@@ -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.
+
+source_set("android") {
+  sources = [
+    "android.S",
+  ]
+}
+
+source_set("linux") {
+  sources = [
+    "linux.S",
+  ]
+}
diff --git a/src/arch/aarch64/boot_flow/android.S b/src/arch/aarch64/boot_flow/android.S
new file mode 100644
index 0000000..13f0530
--- /dev/null
+++ b/src/arch/aarch64/boot_flow/android.S
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+.section .init.plat_boot_flow_hook, "ax"
+.global plat_boot_flow_hook
+plat_boot_flow_hook:
+	/* Do nothing. */
+	ret
diff --git a/src/arch/aarch64/boot_flow/linux.S b/src/arch/aarch64/boot_flow/linux.S
new file mode 100644
index 0000000..581a656
--- /dev/null
+++ b/src/arch/aarch64/boot_flow/linux.S
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+.section .init.plat_boot_flow_hook, "ax"
+.global plat_boot_flow_hook
+plat_boot_flow_hook:
+	/* Save the FDT pointer to a global variable. */
+	adrp x25, plat_fdt_addr
+	add x25, x25, :lo12:plat_fdt_addr
+	str x0, [x25]
+	ret
diff --git a/src/arch/aarch64/hypervisor/hypervisor_entry.S b/src/arch/aarch64/hypervisor/hypervisor_entry.S
index e797074..3ab25c2 100644
--- a/src/arch/aarch64/hypervisor/hypervisor_entry.S
+++ b/src/arch/aarch64/hypervisor/hypervisor_entry.S
@@ -20,7 +20,7 @@
 .global image_entry
 image_entry:
 	/* Interpret the registers passed from the loader. */
-	bl plat_entry
+	bl plat_boot_flow_hook
 
 	/* Get pointer to first cpu. */
 	adrp x0, cpus
diff --git a/src/arch/fake/BUILD.gn b/src/arch/fake/BUILD.gn
index 6eee2b8..a0c7219 100644
--- a/src/arch/fake/BUILD.gn
+++ b/src/arch/fake/BUILD.gn
@@ -19,6 +19,12 @@
   ]
 }
 
+# Empty implementation of platform boot flow.
+# Fake arch targets should not depend on the boot flow functions. Will fail to
+# compile if they do.
+source_set("boot_flow") {
+}
+
 # Fake implementation of putchar logs to the console.
 source_set("console") {
   sources = [
diff --git a/src/boot_flow/BUILD.gn b/src/boot_flow/BUILD.gn
new file mode 100644
index 0000000..9de339d
--- /dev/null
+++ b/src/boot_flow/BUILD.gn
@@ -0,0 +1,41 @@
+# 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.
+
+import("//build/toolchain/platform.gni")
+
+source_set("common") {
+  sources = [
+    "common.c",
+  ]
+}
+
+source_set("android") {
+  sources = [
+    "android.c",
+  ]
+  deps = [
+    ":common",
+    "//src/arch/${plat_arch}/boot_flow:android",
+  ]
+}
+
+source_set("linux") {
+  sources = [
+    "linux.c",
+  ]
+  deps = [
+    ":common",
+    "//src/arch/${plat_arch}/boot_flow:linux",
+  ]
+}
diff --git a/src/boot_flow/android.c b/src/boot_flow/android.c
new file mode 100644
index 0000000..4917ef4
--- /dev/null
+++ b/src/boot_flow/android.c
@@ -0,0 +1,48 @@
+/*
+ * 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/layout.h"
+#include "hf/plat/boot_flow.h"
+
+/**
+ * FDT was compiled into Hafnium. Return physical address of the `.plat.fdt`
+ * section of Hafnium image.
+ */
+paddr_t plat_get_fdt_addr(void)
+{
+	return layout_fdt_begin();
+}
+
+/**
+ * Android boot flow does not use kernel arguments. Pass zero.
+ */
+uintreg_t plat_get_kernel_arg(void)
+{
+	return 0;
+}
+
+/**
+ * Initrd was compiled into Hafnium. Return range of the '.plat.initrd' section.
+ */
+bool plat_get_initrd_range(const struct fdt_node *fdt_root, paddr_t *begin,
+			   paddr_t *end)
+{
+	(void)fdt_root;
+
+	*begin = layout_initrd_begin();
+	*end = layout_initrd_end();
+	return true;
+}
diff --git a/src/boot_flow/common.c b/src/boot_flow/common.c
new file mode 100644
index 0000000..d211df6
--- /dev/null
+++ b/src/boot_flow/common.c
@@ -0,0 +1,68 @@
+/*
+ * 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/boot_flow.h"
+#include "hf/dlog.h"
+#include "hf/fdt_handler.h"
+#include "hf/plat/boot_flow.h"
+
+/**
+ * Parses information from FDT needed to initialize Hafnium.
+ * FDT is mapped at the beginning and unmapped before exiting the function.
+ */
+bool boot_flow_init(struct mm_stage1_locked stage1_locked,
+		    struct manifest *manifest, struct boot_params *boot_params,
+		    struct mpool *ppool)
+{
+	bool ret = false;
+	struct fdt_header *fdt;
+	struct fdt_node fdt_root;
+	enum manifest_return_code manifest_ret;
+
+	/* Get the memory map from the FDT. */
+	fdt = fdt_map(stage1_locked, plat_get_fdt_addr(), &fdt_root, ppool);
+	if (fdt == NULL) {
+		dlog("Unable to map FDT.\n");
+		return false;
+	}
+
+	if (!fdt_find_child(&fdt_root, "")) {
+		dlog("Unable to find FDT root node.\n");
+		goto out_unmap_fdt;
+	}
+
+	manifest_ret = manifest_init(manifest, &fdt_root);
+	if (manifest_ret != MANIFEST_SUCCESS) {
+		dlog("Could not parse manifest: %s.\n",
+		     manifest_strerror(manifest_ret));
+		goto out_unmap_fdt;
+	}
+
+	if (!boot_params_init(boot_params, &fdt_root)) {
+		dlog("Could not parse boot params.\n");
+		goto out_unmap_fdt;
+	}
+
+	ret = true;
+
+out_unmap_fdt:
+	if (!fdt_unmap(stage1_locked, fdt, ppool)) {
+		dlog("Unable to unmap FDT.\n");
+		ret = false;
+	}
+
+	return ret;
+}
diff --git a/src/boot_flow/linux.c b/src/boot_flow/linux.c
new file mode 100644
index 0000000..52fcb8a
--- /dev/null
+++ b/src/boot_flow/linux.c
@@ -0,0 +1,48 @@
+/*
+ * 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/fdt_handler.h"
+#include "hf/plat/boot_flow.h"
+
+/* Set by arch-specific boot-time hook. */
+uintreg_t plat_fdt_addr;
+
+/**
+ * Returns the physical address of board FDT. This was passed to Hafnium in the
+ * first kernel arg by the boot loader.
+ */
+paddr_t plat_get_fdt_addr(void)
+{
+	return pa_init((uintpaddr_t)plat_fdt_addr);
+}
+
+/**
+ * When handing over to the primary, give it the same FDT address that was given
+ * to Hafnium. The FDT may have been modified during Hafnium init.
+ */
+uintreg_t plat_get_kernel_arg(void)
+{
+	return plat_fdt_addr;
+}
+
+/**
+ * Load initrd range from the board FDT.
+ */
+bool plat_get_initrd_range(const struct fdt_node *fdt_root, paddr_t *begin,
+			   paddr_t *end)
+{
+	return fdt_find_initrd(fdt_root, begin, end);
+}
diff --git a/src/boot_params.c b/src/boot_params.c
new file mode 100644
index 0000000..8fbd77d
--- /dev/null
+++ b/src/boot_params.c
@@ -0,0 +1,52 @@
+/*
+ * 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/boot_params.h"
+
+#include "hf/dlog.h"
+#include "hf/fdt_handler.h"
+#include "hf/layout.h"
+#include "hf/manifest.h"
+#include "hf/plat/boot_flow.h"
+
+/**
+ * Extract the boot parameters from the FDT and the boot-flow driver.
+ */
+bool boot_params_init(struct boot_params *p, const struct fdt_node *fdt_root)
+{
+	p->mem_ranges_count = 0;
+	p->kernel_arg = plat_get_kernel_arg();
+
+	return plat_get_initrd_range(fdt_root, &p->initrd_begin,
+				     &p->initrd_end) &&
+	       fdt_find_cpus(fdt_root, p->cpu_ids, &p->cpu_count) &&
+	       fdt_find_memory_ranges(fdt_root, p);
+}
+
+/**
+ * Updates the FDT before being passed to the primary VM's kernel.
+ *
+ * TODO: in future, each VM will declare whether it expects an argument passed
+ * and that will be static data e.g. it will provide its own FDT so there will
+ * be no FDT modification. This is done because each VM has a very different
+ * view of the system and we don't want to force VMs to require loader code when
+ * another loader can load the data for it.
+ */
+bool boot_params_patch_fdt(struct mm_stage1_locked stage1_locked,
+			   struct boot_params_update *p, struct mpool *ppool)
+{
+	return fdt_patch(stage1_locked, plat_get_fdt_addr(), p, ppool);
+}
diff --git a/src/fdt_handler.c b/src/fdt_handler.c
index 900f766..057a2ca 100644
--- a/src/fdt_handler.c
+++ b/src/fdt_handler.c
@@ -80,25 +80,25 @@
 }
 
 /**
- * Finds the memory region where initrd is stored, and updates the fdt node
- * cursor to the node called "chosen".
+ * Finds the memory region where initrd is stored.
  */
-bool fdt_find_initrd(struct fdt_node *n, paddr_t *begin, paddr_t *end)
+bool fdt_find_initrd(const struct fdt_node *root, paddr_t *begin, paddr_t *end)
 {
+	struct fdt_node n = *root;
 	uint64_t initrd_begin;
 	uint64_t initrd_end;
 
-	if (!fdt_find_child(n, "chosen")) {
+	if (!fdt_find_child(&n, "chosen")) {
 		dlog("Unable to find 'chosen'\n");
 		return false;
 	}
 
-	if (!fdt_read_number(n, "linux,initrd-start", &initrd_begin)) {
+	if (!fdt_read_number(&n, "linux,initrd-start", &initrd_begin)) {
 		dlog("Unable to read linux,initrd-start\n");
 		return false;
 	}
 
-	if (!fdt_read_number(n, "linux,initrd-end", &initrd_end)) {
+	if (!fdt_read_number(&n, "linux,initrd-end", &initrd_end)) {
 		dlog("Unable to read linux,initrd-end\n");
 		return false;
 	}
@@ -109,7 +109,7 @@
 	return true;
 }
 
-void fdt_find_cpus(const struct fdt_node *root, cpu_id_t *cpu_ids,
+bool fdt_find_cpus(const struct fdt_node *root, cpu_id_t *cpu_ids,
 		   size_t *cpu_count)
 {
 	struct fdt_node n = *root;
@@ -120,7 +120,7 @@
 
 	if (!fdt_find_child(&n, "cpus")) {
 		dlog("Unable to find 'cpus'\n");
-		return;
+		return false;
 	}
 
 	if (fdt_read_number(&n, "#address-cells", &address_size)) {
@@ -130,7 +130,7 @@
 	}
 
 	if (!fdt_first_child(&n, &name)) {
-		return;
+		return false;
 	}
 
 	do {
@@ -150,12 +150,12 @@
 
 			if (*cpu_count >= MAX_CPUS) {
 				dlog("Found more than %d CPUs\n", MAX_CPUS);
-				return;
+				return false;
 			}
 
 			if (!fdt_parse_number(data, address_size, &value)) {
 				dlog("Could not parse CPU id\n");
-				return;
+				return false;
 			}
 			cpu_ids[(*cpu_count)++] = value;
 
@@ -163,9 +163,11 @@
 			data += address_size;
 		}
 	} while (fdt_next_sibling(&n, &name));
+
+	return true;
 }
 
-void fdt_find_memory_ranges(const struct fdt_node *root, struct boot_params *p)
+bool fdt_find_memory_ranges(const struct fdt_node *root, struct boot_params *p)
 {
 	struct fdt_node n = *root;
 	const char *name;
@@ -191,7 +193,7 @@
 
 	/* Look for nodes with the device_type set to "memory". */
 	if (!fdt_first_child(&n, &name)) {
-		return;
+		return false;
 	}
 
 	do {
@@ -234,6 +236,8 @@
 	p->mem_ranges_count = mem_range_index;
 
 	/* TODO: Check for "reserved-memory" nodes. */
+
+	return true;
 }
 
 struct fdt_header *fdt_map(struct mm_stage1_locked stage1_locked,
diff --git a/src/load.c b/src/load.c
index 341f276..25e593c 100644
--- a/src/load.c
+++ b/src/load.c
@@ -22,7 +22,6 @@
 #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"
@@ -251,16 +250,13 @@
  * Memory reserved for the VMs is added to the `reserved_ranges` of `update`.
  */
 bool load_secondary(struct mm_stage1_locked stage1_locked,
-		    const struct memiter *cpio,
+		    const struct manifest *manifest, const struct memiter *cpio,
 		    const struct boot_params *params,
 		    struct boot_params_update *update, struct mpool *ppool)
 {
 	struct vm *primary;
-	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),
@@ -279,24 +275,13 @@
 			pa_addr(mem_ranges_available[i].end), PAGE_SIZE));
 	}
 
-	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];
+	for (i = 0; i < manifest->num_vms; ++i) {
+		const 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;
+		struct memiter kernel_filename;
 		uint64_t mem_size;
 		paddr_t secondary_mem_begin;
 		paddr_t secondary_mem_end;
@@ -306,17 +291,16 @@
 			continue;
 		}
 
-		dlog("Loading VM%d: ", (int)vm_id);
-		memiter_dlog_str(&manifest_vm->debug_name);
-		dlog(".\n");
+		dlog("Loading VM%d: %s.\n", (int)vm_id,
+		     manifest_vm->debug_name);
 
-		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");
+		memiter_init(&kernel_filename,
+			     manifest_vm->secondary.kernel_filename,
+			     strnlen_s(manifest_vm->secondary.kernel_filename,
+				       MANIFEST_MAX_STRING_LENGTH));
+		if (!memiter_find_file(cpio, &kernel_filename, &kernel)) {
+			dlog("Could not find kernel file \"%s\".\n",
+			     manifest_vm->secondary.kernel_filename);
 			continue;
 		}
 
diff --git a/src/main.c b/src/main.c
index 3ccf6d6..4b2c5b3 100644
--- a/src/main.c
+++ b/src/main.c
@@ -20,6 +20,7 @@
 #include "hf/arch/init.h"
 
 #include "hf/api.h"
+#include "hf/boot_flow.h"
 #include "hf/boot_params.h"
 #include "hf/cpio.h"
 #include "hf/cpu.h"
@@ -43,6 +44,7 @@
  */
 static void one_time_init(void)
 {
+	struct manifest manifest;
 	struct boot_params params;
 	struct boot_params_update update;
 	struct memiter primary_initrd;
@@ -74,9 +76,7 @@
 
 	mm_stage1_locked = mm_lock_stage1();
 
-	if (!plat_get_boot_params(mm_stage1_locked, &params, &ppool)) {
-		panic("unable to retrieve boot params");
-	}
+	boot_flow_init(mm_stage1_locked, &manifest, &params, &ppool);
 
 	cpu_module_init(params.cpu_ids, params.cpu_count);
 
@@ -112,13 +112,13 @@
 	update.initrd_begin = pa_from_va(va_from_ptr(primary_initrd.next));
 	update.initrd_end = pa_from_va(va_from_ptr(primary_initrd.limit));
 	update.reserved_ranges_count = 0;
-	if (!load_secondary(mm_stage1_locked, &cpio, &params, &update,
-			    &ppool)) {
+	if (!load_secondary(mm_stage1_locked, &manifest, &cpio, &params,
+			    &update, &ppool)) {
 		panic("unable to load secondary VMs");
 	}
 
 	/* Prepare to run by updating bootparams as seen by primary VM. */
-	if (!plat_update_boot_params(mm_stage1_locked, &update, &ppool)) {
+	if (!boot_params_patch_fdt(mm_stage1_locked, &update, &ppool)) {
 		panic("plat_update_boot_params failed");
 	}
 
diff --git a/src/manifest.c b/src/manifest.c
index 4cf490a..fb2b695 100644
--- a/src/manifest.c
+++ b/src/manifest.c
@@ -54,8 +54,8 @@
 }
 
 static enum manifest_return_code read_string(const struct fdt_node *node,
-					     const char *property,
-					     struct memiter *out)
+					     const char *property, char *out,
+					     rsize_t out_sz)
 {
 	const char *data;
 	uint32_t size;
@@ -72,7 +72,12 @@
 		return MANIFEST_ERROR_MALFORMED_STRING;
 	}
 
-	memiter_init(out, data, size - 1);
+	/* Check that the string fits into the buffer. */
+	if (size > out_sz) {
+		return MANIFEST_ERROR_STRING_TOO_LONG;
+	}
+
+	memcpy_s(out, out_sz, data, size);
 	return MANIFEST_SUCCESS;
 }
 
@@ -195,10 +200,12 @@
 					  struct manifest_vm *vm,
 					  spci_vm_id_t vm_id)
 {
-	TRY(read_string(node, "debug_name", &vm->debug_name));
+	TRY(read_string(node, "debug_name", vm->debug_name,
+			sizeof(vm->debug_name)));
 	if (vm_id != HF_PRIMARY_VM_ID) {
 		TRY(read_string(node, "kernel_filename",
-				&vm->secondary.kernel_filename));
+				vm->secondary.kernel_filename,
+				sizeof(vm->secondary.kernel_filename)));
 		TRY(read_uint64(node, "mem_size", &vm->secondary.mem_size));
 		TRY(read_uint16(node, "vcpu_count", &vm->secondary.vcpu_count));
 	}
@@ -209,7 +216,7 @@
  * Parse manifest from FDT.
  */
 enum manifest_return_code manifest_init(struct manifest *manifest,
-					struct memiter *fdt)
+					const struct fdt_node *fdt_root)
 {
 	char vm_name_buf[VM_NAME_BUF_SIZE];
 	struct fdt_node hyp_node;
@@ -220,13 +227,7 @@
 	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;
-	}
+	hyp_node = *fdt_root;
 	if (!fdt_find_child(&hyp_node, "hypervisor")) {
 		return MANIFEST_ERROR_NO_HYPERVISOR_FDT_NODE;
 	}
@@ -283,10 +284,6 @@
 	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_NOT_COMPATIBLE:
@@ -302,6 +299,8 @@
 		return "Property not found";
 	case MANIFEST_ERROR_MALFORMED_STRING:
 		return "Malformed string property";
+	case MANIFEST_ERROR_STRING_TOO_LONG:
+		return "String too long";
 	case MANIFEST_ERROR_MALFORMED_STRING_LIST:
 		return "Malformed string list property";
 	case MANIFEST_ERROR_MALFORMED_INTEGER:
diff --git a/src/manifest_test.cc b/src/manifest_test.cc
index 432b8f8..fcd111d 100644
--- a/src/manifest_test.cc
+++ b/src/manifest_test.cc
@@ -30,7 +30,7 @@
 using ::testing::NotNull;
 
 template <typename T>
-void exec(const char *program, char *const args[], const T &stdin,
+void exec(const char *program, const char *args[], const T &stdin,
 	  std::vector<char> *stdout)
 {
 	/* Create two pipes, one for stdin and one for stdout. */
@@ -93,7 +93,7 @@
 		close(parent_write_fd);
 
 		/* Execute the given program. */
-		execv(program, args);
+		execv(program, const_cast<char *const *>(args));
 	}
 }
 
@@ -122,13 +122,14 @@
 
 	std::vector<char> Build()
 	{
-		char *const dtc_args[] = {NULL};
+		const char *program = "./build/image/dtc.py";
+		const char *dtc_args[] = {program, "compile", NULL};
 		std::vector<char> dtc_stdout;
 
 		/* Finish root node. */
 		EndChild();
 
-		exec("./build/image/dtc.py", dtc_args, dts_.str(), &dtc_stdout);
+		exec(program, dtc_args, dts_.str(), &dtc_stdout);
 		return dtc_stdout;
 	}
 
@@ -207,21 +208,31 @@
 	std::stringstream dts_;
 };
 
+static bool get_fdt_root(const std::vector<char> &dtb,
+			 struct fdt_node *fdt_root)
+{
+	const struct fdt_header *fdt_header;
+
+	fdt_header = reinterpret_cast<const struct fdt_header *>(dtb.data());
+	return fdt_root_node(fdt_root, fdt_header) &&
+	       fdt_find_child(fdt_root, "");
+}
+
 TEST(manifest, no_hypervisor_node)
 {
 	struct manifest m;
-	struct memiter it;
+	struct fdt_node fdt_root;
 	std::vector<char> dtb = ManifestDtBuilder().Build();
 
-	memiter_init(&it, dtb.data(), dtb.size());
-	ASSERT_EQ(manifest_init(&m, &it),
+	ASSERT_TRUE(get_fdt_root(dtb, &fdt_root));
+	ASSERT_EQ(manifest_init(&m, &fdt_root),
 		  MANIFEST_ERROR_NO_HYPERVISOR_FDT_NODE);
 }
 
 TEST(manifest, no_compatible_property)
 {
 	struct manifest m;
-	struct memiter it;
+	struct fdt_node fdt_root;
 
 	/* clang-format off */
 	std::vector<char> dtb = ManifestDtBuilder()
@@ -230,14 +241,15 @@
 		.Build();
 	/* clang-format on */
 
-	memiter_init(&it, dtb.data(), dtb.size());
-	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_ERROR_PROPERTY_NOT_FOUND);
+	ASSERT_TRUE(get_fdt_root(dtb, &fdt_root));
+	ASSERT_EQ(manifest_init(&m, &fdt_root),
+		  MANIFEST_ERROR_PROPERTY_NOT_FOUND);
 }
 
 TEST(manifest, not_compatible)
 {
 	struct manifest m;
-	struct memiter it;
+	struct fdt_node fdt_root;
 
 	/* clang-format off */
 	std::vector<char> dtb = ManifestDtBuilder()
@@ -247,14 +259,14 @@
 		.Build();
 	/* clang-format on */
 
-	memiter_init(&it, dtb.data(), dtb.size());
-	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_ERROR_NOT_COMPATIBLE);
+	ASSERT_TRUE(get_fdt_root(dtb, &fdt_root));
+	ASSERT_EQ(manifest_init(&m, &fdt_root), MANIFEST_ERROR_NOT_COMPATIBLE);
 }
 
 TEST(manifest, compatible_one_of_many)
 {
 	struct manifest m;
-	struct memiter it;
+	struct fdt_node fdt_root;
 
 	/* clang-format off */
 	std::vector<char> dtb = ManifestDtBuilder()
@@ -267,14 +279,14 @@
 		.Build();
 	/* clang-format on */
 
-	memiter_init(&it, dtb.data(), dtb.size());
-	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_SUCCESS);
+	ASSERT_TRUE(get_fdt_root(dtb, &fdt_root));
+	ASSERT_EQ(manifest_init(&m, &fdt_root), MANIFEST_SUCCESS);
 }
 
 TEST(manifest, no_vm_nodes)
 {
 	struct manifest m;
-	struct memiter it;
+	struct fdt_node fdt_root;
 
 	/* clang-format off */
 	std::vector<char> dtb = ManifestDtBuilder()
@@ -284,14 +296,48 @@
 		.Build();
 	/* clang-format on */
 
-	memiter_init(&it, dtb.data(), dtb.size());
-	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_ERROR_NO_PRIMARY_VM);
+	ASSERT_TRUE(get_fdt_root(dtb, &fdt_root));
+	ASSERT_EQ(manifest_init(&m, &fdt_root), MANIFEST_ERROR_NO_PRIMARY_VM);
+}
+
+static std::vector<char> gen_long_string_dtb(bool valid)
+{
+	const char last_valid[] = "1234567890123456789012345678901";
+	const char first_invalid[] = "12345678901234567890123456789012";
+	static_assert(sizeof(last_valid) == MANIFEST_MAX_STRING_LENGTH);
+	static_assert(sizeof(first_invalid) == MANIFEST_MAX_STRING_LENGTH + 1);
+
+	/* clang-format off */
+	return ManifestDtBuilder()
+		.StartChild("hypervisor")
+			.Compatible()
+			.StartChild("vm1")
+				.DebugName(valid ? last_valid : first_invalid)
+			.EndChild()
+		.EndChild()
+		.Build();
+	/* clang-format on */
+}
+
+TEST(manifest, long_string)
+{
+	struct manifest m;
+	struct fdt_node fdt_root;
+
+	std::vector<char> dtb_last_valid = gen_long_string_dtb(true);
+	std::vector<char> dtb_first_invalid = gen_long_string_dtb(false);
+
+	ASSERT_TRUE(get_fdt_root(dtb_last_valid, &fdt_root));
+	ASSERT_EQ(manifest_init(&m, &fdt_root), MANIFEST_SUCCESS);
+
+	ASSERT_TRUE(get_fdt_root(dtb_first_invalid, &fdt_root));
+	ASSERT_EQ(manifest_init(&m, &fdt_root), MANIFEST_ERROR_STRING_TOO_LONG);
 }
 
 TEST(manifest, reserved_vm_id)
 {
 	struct manifest m;
-	struct memiter it;
+	struct fdt_node fdt_root;
 
 	/* clang-format off */
 	std::vector<char> dtb = ManifestDtBuilder()
@@ -310,8 +356,8 @@
 		.Build();
 	/* clang-format on */
 
-	memiter_init(&it, dtb.data(), dtb.size());
-	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_ERROR_RESERVED_VM_ID);
+	ASSERT_TRUE(get_fdt_root(dtb, &fdt_root));
+	ASSERT_EQ(manifest_init(&m, &fdt_root), MANIFEST_ERROR_RESERVED_VM_ID);
 }
 
 static std::vector<char> gen_vcpu_count_limit_dtb(uint64_t vcpu_count)
@@ -337,25 +383,26 @@
 TEST(manifest, vcpu_count_limit)
 {
 	struct manifest m;
-	struct memiter it;
+	struct fdt_node fdt_root;
 	std::vector<char> dtb_last_valid = gen_vcpu_count_limit_dtb(UINT16_MAX);
 	std::vector<char> dtb_first_invalid =
 		gen_vcpu_count_limit_dtb(UINT16_MAX + 1);
 
-	memiter_init(&it, dtb_last_valid.data(), dtb_last_valid.size());
-	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_SUCCESS);
+	ASSERT_TRUE(get_fdt_root(dtb_last_valid, &fdt_root));
+	ASSERT_EQ(manifest_init(&m, &fdt_root), MANIFEST_SUCCESS);
 	ASSERT_EQ(m.num_vms, 2);
 	ASSERT_EQ(m.vm[1].secondary.vcpu_count, UINT16_MAX);
 
-	memiter_init(&it, dtb_first_invalid.data(), dtb_first_invalid.size());
-	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_ERROR_INTEGER_OVERFLOW);
+	ASSERT_TRUE(get_fdt_root(dtb_first_invalid, &fdt_root));
+	ASSERT_EQ(manifest_init(&m, &fdt_root),
+		  MANIFEST_ERROR_INTEGER_OVERFLOW);
 }
 
 TEST(manifest, valid)
 {
 	struct manifest m;
 	struct manifest_vm *vm;
-	struct memiter it;
+	struct fdt_node fdt_root;
 
 	/* clang-format off */
 	std::vector<char> dtb = ManifestDtBuilder()
@@ -380,27 +427,25 @@
 		.Build();
 	/* clang-format on */
 
-	memiter_init(&it, dtb.data(), dtb.size());
+	ASSERT_TRUE(get_fdt_root(dtb, &fdt_root));
 
-	ASSERT_EQ(manifest_init(&m, &it), MANIFEST_SUCCESS);
+	ASSERT_EQ(manifest_init(&m, &fdt_root), MANIFEST_SUCCESS);
 	ASSERT_EQ(m.num_vms, 3);
 
 	vm = &m.vm[0];
-	ASSERT_TRUE(memiter_iseq(&vm->debug_name, "primary_vm"));
+	ASSERT_STREQ(vm->debug_name, "primary_vm");
 
 	vm = &m.vm[1];
-	ASSERT_TRUE(memiter_iseq(&vm->debug_name, "first_secondary_vm"));
+	ASSERT_STREQ(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"));
+	ASSERT_STREQ(vm->secondary.kernel_filename, "first_kernel");
 
 	vm = &m.vm[2];
-	ASSERT_TRUE(memiter_iseq(&vm->debug_name, "second_secondary_vm"));
+	ASSERT_STREQ(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"));
+	ASSERT_STREQ(vm->secondary.kernel_filename, "second_kernel");
 }
 
 } /* namespace */
diff --git a/src/memiter.c b/src/memiter.c
index 3331d37..d72250c 100644
--- a/src/memiter.c
+++ b/src/memiter.c
@@ -94,19 +94,6 @@
 }
 
 /**
- * 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)
diff --git a/src/plat.c b/src/plat.c
deleted file mode 100644
index d6c5a4a..0000000
--- a/src/plat.c
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright 2018 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/boot_params.h"
-#include "hf/dlog.h"
-#include "hf/fdt_handler.h"
-#include "hf/layout.h"
-
-/**
- * Default implementation assumes the FDT has been linked into the image.
- *
- * This can be overridden e.g. to provide a fixed address or an address passed
- * by the loader.
- */
-#pragma weak plat_get_fdt_addr
-paddr_t plat_get_fdt_addr(void)
-{
-	return layout_fdt_begin();
-}
-
-/**
- * Default implementation assumes the initrd has been linked into the image.
- *
- * This can be overridden e.g. to provide a fixed address or an address passed
- * by the loader.
- */
-#pragma weak plat_get_initrd_range
-void plat_get_initrd_range(struct mm_stage1_locked stage1_locked,
-			   paddr_t *begin, paddr_t *end, struct mpool *ppool)
-{
-	(void)stage1_locked;
-	(void)ppool;
-
-	*begin = layout_initrd_begin();
-	*end = layout_initrd_end();
-}
-
-/**
- * Default implementation assumes the FDT address is passed to the kernel.
- *
- * TODO: make this part of the VM configuration as secondary VMs will also need
- * to take arguments.
- */
-#pragma weak plat_get_kernel_arg
-uintreg_t plat_get_kernel_arg(void)
-{
-	return (uintreg_t)pa_addr(plat_get_fdt_addr());
-}
-
-/**
- * Default implementation extracts the boot parameters from the FDT but the
- * initrd is provided separately.
- */
-#pragma weak plat_get_boot_params
-bool plat_get_boot_params(struct mm_stage1_locked stage1_locked,
-			  struct boot_params *p, struct mpool *ppool)
-{
-	struct fdt_header *fdt;
-	struct fdt_node n;
-	bool ret = false;
-
-	plat_get_initrd_range(stage1_locked, &p->initrd_begin, &p->initrd_end,
-			      ppool);
-	p->kernel_arg = plat_get_kernel_arg();
-
-	/* Get the memory map from the FDT. */
-	fdt = fdt_map(stage1_locked, plat_get_fdt_addr(), &n, ppool);
-	if (!fdt) {
-		return false;
-	}
-
-	if (!fdt_find_child(&n, "")) {
-		dlog("Unable to find FDT root node.\n");
-		goto out_unmap_fdt;
-	}
-
-	fdt_find_cpus(&n, p->cpu_ids, &p->cpu_count);
-
-	p->mem_ranges_count = 0;
-	fdt_find_memory_ranges(&n, p);
-
-	ret = true;
-
-out_unmap_fdt:
-	if (!fdt_unmap(stage1_locked, fdt, ppool)) {
-		dlog("Unable to unmap fdt.");
-		return false;
-	}
-
-	return ret;
-}
-
-/**
- * Default implementation updates the FDT which is the argument passed to the
- * primary VM's kernel.
- *
- * TODO: in future, each VM will declare whether it expects an argument passed
- * and that will be static data e.g. it will provide its own FDT so there will
- * be no FDT modification. This is done because each VM has a very different
- * view of the system and we don't want to force VMs to require loader code when
- * another loader can load the data for it.
- */
-#pragma weak plat_update_boot_params
-bool plat_update_boot_params(struct mm_stage1_locked stage1_locked,
-			     struct boot_params_update *p, struct mpool *ppool)
-{
-	return fdt_patch(stage1_locked, plat_get_fdt_addr(), p, ppool);
-}
diff --git a/test/hftest/hftest.py b/test/hftest/hftest.py
index 4192cef..a234a99 100755
--- a/test/hftest/hftest.py
+++ b/test/hftest/hftest.py
@@ -113,6 +113,7 @@
         "artifacts",
         "kernel",
         "initrd",
+        "manifest",
         "vm_args",
     ])
 
@@ -121,9 +122,16 @@
 # a single invocation of the target platform.
 DriverRunState = collections.namedtuple("DriverRunState", [
         "log_path",
+        "ret_code",
     ])
 
 
+class DriverRunException(Exception):
+    """Exception thrown if subprocess invoked by a driver returned non-zero
+    status code. Used to fast-exit from a driver command sequence."""
+    pass
+
+
 class Driver:
     """Parent class of drivers for all testable platforms."""
 
@@ -137,28 +145,34 @@
     def start_run(self, run_name):
         """Hook called by Driver subclasses before they invoke the target
         platform."""
-        return DriverRunState(self.args.artifacts.create_file(run_name, ".log"))
+        return DriverRunState(
+                self.args.artifacts.create_file(run_name, ".log"), 0)
 
     def exec_logged(self, run_state, exec_args):
         """Run a subprocess on behalf of a Driver subclass and append its
         stdout and stderr to the main log."""
+        assert(run_state.ret_code == 0)
         with open(run_state.log_path, "a") as f:
             f.write("$ {}\r\n".format(" ".join(exec_args)))
             f.flush()
-            return subprocess.call(exec_args, stdout=f, stderr=f)
+            ret_code = subprocess.call(exec_args, stdout=f, stderr=f)
+            if ret_code != 0:
+                run_state = DriverRunState(run_state.log_path, ret_code)
+                raise DriverRunException()
 
-    def finish_run(self, run_state, ret_code):
+    def finish_run(self, run_state):
         """Hook called by Driver subclasses after they finished running the
         target platform. `ret_code` argument is the return code of the main
         command run by the driver. A corresponding log message is printed."""
         # Decode return code and add a message to the log.
         with open(run_state.log_path, "a") as f:
-            if ret_code == 124:
+            if run_state.ret_code == 124:
                 f.write("\r\n{}{} timed out\r\n".format(
                     HFTEST_LOG_PREFIX, HFTEST_LOG_FAILURE_PREFIX))
-            elif ret_code != 0:
+            elif run_state.ret_code != 0:
                 f.write("\r\n{}{} process return code {}\r\n".format(
-                    HFTEST_LOG_PREFIX, HFTEST_LOG_FAILURE_PREFIX, ret_code))
+                    HFTEST_LOG_PREFIX, HFTEST_LOG_FAILURE_PREFIX,
+                    run_state.ret_code))
 
         # Append log of this run to full test log.
         log_content = read_file(run_state.log_path)
@@ -167,6 +181,14 @@
             log_content + "\r\n\r\n")
         return log_content
 
+    def overlay_dtb(self, run_state, base_dtb, overlay_dtb, out_dtb):
+        """Overlay `overlay_dtb` over `base_dtb` into `out_dtb`."""
+        dtc_args = [
+            DTC_SCRIPT, "overlay",
+            out_dtb, base_dtb, overlay_dtb,
+        ]
+        self.exec_logged(run_state, dtc_args)
+
 
 class QemuDriver(Driver):
     """Driver which runs tests in QEMU."""
@@ -174,7 +196,7 @@
     def __init__(self, args):
         Driver.__init__(self, args)
 
-    def gen_exec_args(self, test_args):
+    def gen_exec_args(self, test_args, dtb_path=None, dumpdtb_path=None):
         """Generate command line arguments for QEMU."""
         exec_args = [
             "timeout", "--foreground", "10s",
@@ -186,6 +208,12 @@
             "-kernel", self.args.kernel,
         ]
 
+        if dtb_path:
+            exec_args += ["-dtb", dtb_path]
+
+        if dumpdtb_path:
+            exec_args += ["-machine", "dumpdtb=" + dumpdtb_path]
+
         if self.args.initrd:
             exec_args += ["-initrd", self.args.initrd]
 
@@ -195,11 +223,33 @@
 
         return exec_args
 
+    def dump_dtb(self, run_state, test_args, path):
+        dumpdtb_args = self.gen_exec_args(test_args, dumpdtb_path=path)
+        self.exec_logged(run_state, dumpdtb_args)
+
     def run(self, run_name, test_args):
         """Run test given by `test_args` in QEMU."""
         run_state = self.start_run(run_name)
-        ret_code = self.exec_logged(run_state, self.gen_exec_args(test_args))
-        return self.finish_run(run_state, ret_code)
+
+        try:
+            dtb_path = None
+
+            # If manifest DTBO specified, dump DTB from QEMU and overlay them.
+            if self.args.manifest:
+                base_dtb_path = self.args.artifacts.create_file(
+                    run_name, ".base.dtb")
+                dtb_path = self.args.artifacts.create_file(run_name, ".dtb")
+                self.dump_dtb(run_state, test_args, base_dtb_path)
+                self.overlay_dtb(
+                    run_state, base_dtb_path, self.args.manifest, dtb_path)
+
+            # Execute test in QEMU..
+            exec_args = self.gen_exec_args(test_args, dtb_path=dtb_path)
+            self.exec_logged(run_state, exec_args)
+        except DriverRunException:
+            pass
+
+        return self.finish_run(run_state)
 
 
 class FvpDriver(Driver):
@@ -272,7 +322,8 @@
     def run(self, run_name, test_args):
         run_state = self.start_run(run_name)
 
-        dts_path = self.args.artifacts.create_file(run_name, ".dts")
+        base_dts_path = self.args.artifacts.create_file(run_name, ".base.dts")
+        base_dtb_path = self.args.artifacts.create_file(run_name, ".base.dtb")
         dtb_path = self.args.artifacts.create_file(run_name, ".dtb")
         uart0_log_path = self.args.artifacts.create_file(run_name, ".uart0.log")
         uart1_log_path = self.args.artifacts.create_file(run_name, ".uart1.log")
@@ -283,20 +334,33 @@
         else:
             initrd_end = 0x85000000  # Default value
 
-        # Create a FDT to pass to FVP.
-        self.gen_dts(dts_path, test_args, initrd_start, initrd_end)
-        dtc_args = [ DTC_SCRIPT, "-i", dts_path, "-o", dtb_path ]
-        self.exec_logged(run_state, dtc_args)
+        try:
+            # Create a DT to pass to FVP.
+            self.gen_dts(base_dts_path, test_args, initrd_start, initrd_end)
 
-        # Run FVP.
-        fvp_args = self.gen_fvp_args(
-            initrd_start, uart0_log_path, uart1_log_path, dtb_path)
-        ret_code = self.exec_logged(run_state, fvp_args)
+            # Compile DTS to DTB.
+            dtc_args = [
+                DTC_SCRIPT, "compile", "-i", base_dts_path, "-o", base_dtb_path,
+            ]
+            self.exec_logged(run_state, dtc_args)
+
+            # If manifest DTBO specified, overlay it.
+            if self.args.manifest:
+                self.overlay_dtb(
+                    run_state, base_dtb_path, self.args.manifest, dtb_path)
+            else:
+                dtb_path = base_dtb_path
+
+            # Run FVP.
+            fvp_args = self.gen_fvp_args(
+                initrd_start, uart0_log_path, uart1_log_path, dtb_path)
+            self.exec_logged(run_state, fvp_args)
+        except DriverRunException:
+            pass
 
         # Append UART0 output to main log.
         append_file(run_state.log_path, read_file(uart0_log_path))
-
-        return self.finish_run(run_state, ret_code)
+        return self.finish_run(run_state)
 
 
 # Tuple used to return information about the results of running a set of tests.
@@ -452,9 +516,12 @@
     # Resolve some paths.
     image = os.path.join(args.out, args.image + ".bin")
     initrd = None
+    manifest = None
     image_name = args.image
     if args.initrd:
-        initrd = os.path.join(args.out_initrd, "obj", args.initrd, "initrd.img")
+        initrd_dir = os.path.join(args.out_initrd, "obj", args.initrd)
+        initrd = os.path.join(initrd_dir, "initrd.img")
+        manifest = os.path.join(initrd_dir, "manifest.dtbo")
         image_name += "_" + args.initrd
     vm_args = args.vm_args or ""
 
@@ -462,7 +529,7 @@
     artifacts = ArtifactsManager(os.path.join(args.log, image_name))
 
     # Create a driver for the platform we want to test on.
-    driver_args = DriverArgs(artifacts, image, initrd, vm_args)
+    driver_args = DriverArgs(artifacts, image, initrd, manifest, vm_args)
     if args.fvp:
         driver = FvpDriver(driver_args)
     else:
diff --git a/test/linux/manifest.dts b/test/linux/manifest.dts
index f863c6e..1372663 100644
--- a/test/linux/manifest.dts
+++ b/test/linux/manifest.dts
@@ -15,8 +15,9 @@
  */
 
 /dts-v1/;
+/plugin/;
 
-/ {
+&{/} {
 	hypervisor {
 		compatible = "hafnium,hafnium";
 		vm1 {
diff --git a/test/vmapi/gicv3/manifest.dts b/test/vmapi/gicv3/manifest.dts
index da3cc13..4e3a769 100644
--- a/test/vmapi/gicv3/manifest.dts
+++ b/test/vmapi/gicv3/manifest.dts
@@ -15,8 +15,9 @@
  */
 
 /dts-v1/;
+/plugin/;
 
-/ {
+&{/} {
 	hypervisor {
 		compatible = "hafnium,hafnium";
 		vm1 {
diff --git a/test/vmapi/primary_only/manifest.dts b/test/vmapi/primary_only/manifest.dts
index 11d7edf..3cf090d 100644
--- a/test/vmapi/primary_only/manifest.dts
+++ b/test/vmapi/primary_only/manifest.dts
@@ -15,8 +15,9 @@
  */
 
 /dts-v1/;
+/plugin/;
 
-/ {
+&{/} {
 	hypervisor {
 		compatible = "hafnium,hafnium";
 		vm1 {
diff --git a/test/vmapi/primary_with_secondaries/manifest.dts b/test/vmapi/primary_with_secondaries/manifest.dts
index ac1530e..6460a60 100644
--- a/test/vmapi/primary_with_secondaries/manifest.dts
+++ b/test/vmapi/primary_with_secondaries/manifest.dts
@@ -15,8 +15,9 @@
  */
 
 /dts-v1/;
+/plugin/;
 
-/ {
+&{/} {
 	hypervisor {
 		compatible = "hafnium,hafnium";
 		vm1 {