Infrastructure for running tests under Linux in primary VM.

Includes an initial test of simply inserting and removing the Hafnium
kernel module.

Change-Id: I832a30d902f58ca71f89374300ab39b2ba3ab877
diff --git a/BUILD.gn b/BUILD.gn
index 3cbad6e..21d1869 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -30,3 +30,17 @@
     "//project/${project}:test_root",
   ]
 }
+
+group("update_prebuilts") {
+  deps = [
+    "//third_party:linux",
+  ]
+}
+
+group("default") {
+  testonly = true
+  deps = [
+    ":root",
+    ":test_root",
+  ]
+}
diff --git a/Makefile b/Makefile
index 56cfcb8..619ac1c 100644
--- a/Makefile
+++ b/Makefile
@@ -86,3 +86,10 @@
 	@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
+
+.PHONY: update-prebuilts
+update-prebuilts: prebuilts/linux-aarch64/linux/vmlinuz
+
+prebuilts/linux-aarch64/linux/vmlinuz: $(OUT_DIR)/build.ninja
+	@$(NINJA) -C $(OUT_DIR) "third_party:linux"
+	cp out/reference/obj/third_party/linux.bin $@
diff --git a/build/image/generate_initrd.py b/build/image/generate_initrd.py
index a49865a..6fb76de 100644
--- a/build/image/generate_initrd.py
+++ b/build/image/generate_initrd.py
@@ -39,15 +39,14 @@
     parser.add_argument("--staging", required=True)
     parser.add_argument("--output", required=True)
     args = parser.parse_args()
+    staged_files = ["vmlinuz", "initrd.img"]
     # Prepare the primary VM image.
-    staged_files = ["vmlinuz"]
     shutil.copyfile(args.primary_vm, os.path.join(args.staging, "vmlinuz"))
-    # Prepare the primary VM's initrd. Currently, it just makes an empty one.
+    # Prepare the primary VM's initrd.
     if args.primary_vm_initrd:
-        raise NotImplementedError(
-            "This doesn't copy the primary VM's initrd yet")
-    with open(os.path.join(args.staging, "initrd.img"), "w") as vms_txt:
-        staged_files.append("initrd.img")
+        shutil.copyfile(args.primary_vm_initrd, os.path.join(args.staging, "initrd.img"))
+    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")
diff --git a/build/image/generate_linux_initrd.py b/build/image/generate_linux_initrd.py
new file mode 100644
index 0000000..06f6502
--- /dev/null
+++ b/build/image/generate_linux_initrd.py
@@ -0,0 +1,47 @@
+#!/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.
+
+"""Generate an initial RAM disk for a Linux VM."""
+
+import argparse
+import os
+import shutil
+import subprocess
+import sys
+
+def Main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--staging", required=True)
+    parser.add_argument("--output", required=True)
+    args = parser.parse_args()
+    # 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
+        # include the path.
+        os.chdir(args.staging)
+        staged_files = [os.path.join(root, filename)
+          for (root, dirs, files) in os.walk(".") for filename in files + dirs]
+        cpio = subprocess.Popen(
+            ["cpio", "--create", "--format=newc"],
+            stdin=subprocess.PIPE,
+            stdout=initrd,
+            stderr=subprocess.PIPE)
+        cpio.communicate(input="\n".join(staged_files).encode("utf-8"))
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(Main())
diff --git a/build/image/image.gni b/build/image/image.gni
index 67af5a3..61a407a 100644
--- a/build/image/image.gni
+++ b/build/image/image.gni
@@ -112,10 +112,47 @@
   }
 }
 
+# Build the initial RAM disk for the Linux VM.
+template("linux_initrd") {
+  initrd_base = "${target_out_dir}/${target_name}/initrd"
+  initrd_file = "${initrd_base}.img"
+  initrd_staging = "${initrd_base}"
+
+  copy("${target_name}__staging") {
+    forward_variables_from(invoker,
+                           [
+                             "testonly",
+                             "sources",
+                             "deps",
+                           ])
+    outputs = [
+      "${initrd_staging}/{{source_file_part}}",
+    ]
+  }
+
+  action(target_name) {
+    forward_variables_from(invoker, [ "testonly" ])
+    script = "//build/image/generate_linux_initrd.py"
+    args = [
+      "--staging",
+      rebase_path(initrd_staging),
+      "--output",
+      rebase_path(initrd_file),
+    ]
+    deps = [
+      ":${target_name}__staging",
+    ]
+    outputs = [
+      initrd_file,
+    ]
+  }
+}
+
 # Build the initial RAM disk for the hypervisor.
 template("initrd") {
-  assert(defined(invoker.primary_vm),
-         "initrd() must specify a \"primary_vm\" value")
+  assert(
+      defined(invoker.primary_vm) || defined(invoker.primary_vm_prebuilt),
+      "initrd() must specify a \"primary_vm\" or \"primary_vm_prebuilt\" value")
 
   action(target_name) {
     forward_variables_from(invoker, [ "testonly" ])
@@ -125,19 +162,32 @@
     initrd_file = "${initrd_base}.img"
     initrd_staging = "${initrd_base}"
 
-    deps = [
-      invoker.primary_vm,
-    ]
+    deps = []
 
-    primary_vm_outputs = get_target_outputs(invoker.primary_vm)
+    if (defined(invoker.primary_vm_prebuilt)) {
+      primary_vm_output = invoker.primary_vm_prebuilt
+    } else {
+      primary_vm_output =
+          get_label_info(invoker.primary_vm, "target_out_dir") + "/" +
+          get_label_info(invoker.primary_vm, "name") + ".bin"
+      deps += [ invoker.primary_vm ]
+    }
     args = [
       "--primary_vm",
-      rebase_path(primary_vm_outputs[0]),
+      rebase_path(primary_vm_output),
       "--staging",
       rebase_path(initrd_staging),
       "--output",
       rebase_path(initrd_file),
     ]
+    if (defined(invoker.primary_initrd)) {
+      deps += [ invoker.primary_initrd ]
+      primary_initrd_outputs = get_target_outputs(invoker.primary_initrd)
+      args += [
+        "--primary_vm_initrd",
+        rebase_path(primary_initrd_outputs[0]),
+      ]
+    }
 
     # Add the info about the secondary VMs. The information about the VMs is
     # encoded in lists with the following elements:
diff --git a/build/make.py b/build/make.py
new file mode 100644
index 0000000..d4cc0d8
--- /dev/null
+++ b/build/make.py
@@ -0,0 +1,44 @@
+#!/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.
+
+"""Runs make to build a target."""
+
+import argparse
+import os
+import shutil
+import subprocess
+import sys
+
+
+def Main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--directory", required=True)
+    parser.add_argument("--out_file", required=True)
+    parser.add_argument("--copy_out_file", required=True)
+    args, make_args = parser.parse_known_args()
+
+    os.chdir(args.directory)
+    os.environ["PWD"] = args.directory
+    status = subprocess.call(["make"] + make_args)
+    if status != 0:
+        return status
+
+    shutil.copyfile(args.out_file, args.copy_out_file)
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(Main())
diff --git a/build/toolchain/BUILD.gn b/build/toolchain/BUILD.gn
index 861bd18..0bfc56b 100644
--- a/build/toolchain/BUILD.gn
+++ b/build/toolchain/BUILD.gn
@@ -12,8 +12,31 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import("//build/toolchain/embedded.gni")
 import("//build/toolchain/host.gni")
 
 host_toolchain("host") {
   use_platform = false
 }
+
+embedded_clang_toolchain("aarch64_linux_clang") {
+  target = "aarch64-linux-musleabi"
+
+  # TODO: Remove //inc/system if we can stop using the version of stdatomic.h
+  # from the Android prebuilt Clang.
+  extra_cflags =
+      "-nostdinc -isystem" +
+      rebase_path("//prebuilts/linux-aarch64/musl/include") + " -isystem" +
+      rebase_path("//prebuilts/linux-x64/clang/lib64/clang/8.0.4/include") +
+      " -isystem" + rebase_path("//inc/system")
+  extra_defines = "-D_LIBCPP_HAS_MUSL_LIBC=1 -D_GNU_SOURCE=1"
+  extra_ldflags = "-no-pie -lc --library-path=" +
+                  rebase_path("//prebuilts/linux-aarch64/musl/lib/") + " " +
+                  rebase_path("//prebuilts/linux-aarch64/musl/lib/crt1.o") +
+                  " " + rebase_path(
+                      "//prebuilts/linux-x64/clang/lib64/clang/8.0.4/lib/linux/libclang_rt.builtins-aarch64-android.a")
+  toolchain_args = {
+    use_platform = true
+    plat_arch = "fake"
+  }
+}
diff --git a/driver/BUILD.gn b/driver/BUILD.gn
new file mode 100644
index 0000000..f6f4119
--- /dev/null
+++ b/driver/BUILD.gn
@@ -0,0 +1,39 @@
+# 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.
+
+action("linux") {
+  script = "//build/make.py"
+  args = [
+    "--directory",
+    rebase_path("linux"),
+    "--out_file",
+    "hafnium.ko",
+    "--copy_out_file",
+    rebase_path("${target_out_dir}/linux/hafnium.ko"),
+    "CC=" + rebase_path("//prebuilts/linux-x64/clang/bin/clang"),
+    "ARCH=arm64",
+    "CROSS_COMPILE=aarch64-linux-gnu-",
+  ]
+  sources = [
+    "linux/Makefile",
+    "linux/hf_call.S",
+    "linux/main.c",
+  ]
+  outputs = [
+    "${target_out_dir}/linux/hafnium.ko",
+  ]
+  deps = [
+    "//third_party:linux_defconfig",
+  ]
+}
diff --git a/inc/system/sys/cdefs.h b/inc/system/sys/cdefs.h
new file mode 100644
index 0000000..ef2bc8f
--- /dev/null
+++ b/inc/system/sys/cdefs.h
@@ -0,0 +1,26 @@
+/*
+ * 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
+
+/*
+ * Empty file to make Android Clang stdatomic.h happy. It includes this internal
+ * glibc header which we don't have, but doesn't actually need it.
+ * TODO: Investigate why Android have replaced the upstream Clang version of
+ * stdatomic.h with one that appears to be from FreeBSD, possibly via Bionic, in
+ * their prebuilt version of Clang. If we can just use the upstream Clang we can
+ * probably remove this workaround.
+ */
diff --git a/kokoro/ubuntu/build.sh b/kokoro/ubuntu/build.sh
index 0d3a909..855a849 100755
--- a/kokoro/ubuntu/build.sh
+++ b/kokoro/ubuntu/build.sh
@@ -36,6 +36,15 @@
 
 CLANG=${PWD}/prebuilts/linux-x64/clang/bin/clang
 
+# Kokoro does something weird that makes all files look dirty to git diff-index;
+# this fixes it so that the Linux build doesn't think it has a dirty tree for
+# building the Hafnium kernel module (and so end up with a version magic string
+# that doesn't match the prebuilt kernel).
+(
+	cd third_party/linux &&
+	git status
+)
+
 #
 # Step 1: make sure it builds.
 #
@@ -95,12 +104,11 @@
 	exit 1
 fi
 
-# Step 7: make sure the Linux driver builds and maintains style.
+# Step 7: make sure the Linux driver maintains style. It's already built as part
+# of the tests.
 (
 export ARCH=arm64 &&
 export CROSS_COMPILE=aarch64-linux-gnu- &&
-make CC=${CLANG} -C third_party/linux defconfig modules_prepare &&
 cd driver/linux &&
-make CC=${CLANG} &&
 make checkpatch
 )
diff --git a/kokoro/ubuntu/test.sh b/kokoro/ubuntu/test.sh
index 8940e0b..58bc932 100755
--- a/kokoro/ubuntu/test.sh
+++ b/kokoro/ubuntu/test.sh
@@ -45,3 +45,4 @@
 $HFTEST hafnium --initrd test/vmapi/gicv3/gicv3_test
 $HFTEST hafnium --initrd test/vmapi/primary_only/primary_only_test
 $HFTEST hafnium --initrd test/vmapi/primary_with_secondaries/primary_with_secondaries_test
+$HFTEST hafnium --initrd test/linux/linux_test --vm_args "rdinit=/test_binary --"
diff --git a/prebuilts b/prebuilts
index aecc66e..1521f63 160000
--- a/prebuilts
+++ b/prebuilts
@@ -1 +1 @@
-Subproject commit aecc66e960abf4c22c98b7c3f4cd27a829d64e9e
+Subproject commit 1521f631f051a02d4914d585e4bda39d97deb9af
diff --git a/project/reference b/project/reference
index dc8ad20..bf3991c 160000
--- a/project/reference
+++ b/project/reference
@@ -1 +1 @@
-Subproject commit dc8ad20586c6419ee62cc9f4b02b1dab4b9f240c
+Subproject commit bf3991cb9e86b2b91775365b2cb634a169b3dbd6
diff --git a/src/arch/fake/hftest/BUILD.gn b/src/arch/fake/hftest/BUILD.gn
new file mode 100644
index 0000000..c69041f
--- /dev/null
+++ b/src/arch/fake/hftest/BUILD.gn
@@ -0,0 +1,23 @@
+# 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.
+
+# These components are only used by tests running until Linux VMs.
+
+# Shutdown the system.
+source_set("power_mgmt") {
+  testonly = true
+  sources = [
+    "power_mgmt.c",
+  ]
+}
diff --git a/src/arch/fake/hftest/power_mgmt.c b/src/arch/fake/hftest/power_mgmt.c
new file mode 100644
index 0000000..a171036
--- /dev/null
+++ b/src/arch/fake/hftest/power_mgmt.c
@@ -0,0 +1,27 @@
+/*
+ * 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/arch/vm/power_mgmt.h"
+
+#include <sys/reboot.h>
+
+noreturn void arch_power_off(void)
+{
+	reboot(RB_POWER_OFF);
+	for (;;) {
+		/* This should never be reached. */
+	}
+}
diff --git a/src/arch/fake/inc/hf/arch/vm/power_mgmt.h b/src/arch/fake/inc/hf/arch/vm/power_mgmt.h
new file mode 100644
index 0000000..607705e
--- /dev/null
+++ b/src/arch/fake/inc/hf/arch/vm/power_mgmt.h
@@ -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.
+ */
+
+#pragma once
+
+#include <stdnoreturn.h>
+
+noreturn void arch_power_off(void);
diff --git a/test/hftest/BUILD.gn b/test/hftest/BUILD.gn
index 561fc2d..9f1df7c 100644
--- a/test/hftest/BUILD.gn
+++ b/test/hftest/BUILD.gn
@@ -61,6 +61,23 @@
   ]
 }
 
+# Testing framework for tests running under Linux in the primary VM.
+source_set("hftest_linux") {
+  testonly = true
+  public_configs = [ ":hftest_config" ]
+
+  sources = [
+    "linux_main.c",
+  ]
+
+  deps = [
+    ":common",
+    "//src:dlog",
+    "//src:memiter",
+    "//src/arch/${plat_arch}/hftest:power_mgmt",
+  ]
+}
+
 source_set("hftest_standalone") {
   visibility = [ ":*" ]
   testonly = true
@@ -68,16 +85,31 @@
   public_configs = [ ":hftest_config" ]
 
   sources = [
-    "hftest.c",
+    "standalone_main.c",
   ]
 
   deps = [
+    ":common",
     "//src:dlog",
     "//src:fdt",
     "//src:memiter",
     "//src/arch/${plat_arch}:entry",
-    "//src/arch/${plat_arch}:std",
     "//src/arch/${plat_arch}/hftest:entry",
     "//src/arch/${plat_arch}/hftest:power_mgmt",
   ]
 }
+
+# Common code for hftest, whether it is running under Linux, under Hafnium in
+# the primary VM, or directly on the hardware.
+source_set("common") {
+  visibility = [ ":*" ]
+  testonly = true
+  public_configs = [ ":hftest_config" ]
+  sources = [
+    "hftest_common.c",
+  ]
+  deps = [
+    "//src:memiter",
+    "//src/arch/${plat_arch}:std",
+  ]
+}
diff --git a/test/hftest/hftest.py b/test/hftest/hftest.py
index bc17b70..a124682 100755
--- a/test/hftest/hftest.py
+++ b/test/hftest/hftest.py
@@ -34,9 +34,9 @@
 
 def qemu(image, initrd, args, log):
     qemu_args = [
-        "timeout", "--foreground", "5s",
+        "timeout", "--foreground", "10s",
         "./prebuilts/linux-x64/qemu/qemu-system-aarch64", "-M", "virt,gic_version=3",
-        "-cpu", "cortex-a57", "-smp", "4", "-m", "16M", "-machine", "virtualization=true",
+        "-cpu", "cortex-a57", "-smp", "4", "-m", "64M", "-machine", "virtualization=true",
         "-nographic", "-nodefaults", "-serial", "stdio", "-kernel", image,
     ]
     if initrd:
@@ -78,6 +78,7 @@
     parser.add_argument("--initrd")
     parser.add_argument("--suite")
     parser.add_argument("--test")
+    parser.add_argument("--vm_args")
     args = parser.parse_args()
     # Resolve some paths.
     image = os.path.join(args.out, args.image + ".bin")
@@ -86,13 +87,14 @@
     if args.initrd:
         initrd = os.path.join(args.out, "obj", args.initrd, "initrd.img")
         suite += "_" + args.initrd
+    vm_args = args.vm_args or ""
     log = os.path.join(args.log, suite)
     ensure_dir(log)
     print("Logs saved under", log)
     log_file = os.path.join(log, "sponge_log.log")
     with open(log_file, "w") as sponge_log:
         # Query the tests in the image.
-        out = qemu(image, initrd, "json", os.path.join(log, "json.log"))
+        out = qemu(image, initrd, vm_args + " json", os.path.join(log, "json.log"))
         sponge_log.write(out)
         sponge_log.write("\r\n\r\n")
         hftest_json = "\n".join(hftest_lines(out))
@@ -127,7 +129,7 @@
                 print("      RUN", test)
                 test_log = os.path.join(log,
                                         suite["name"] + "." + test + ".log")
-                out = qemu(image, initrd, "run {} {}".format(
+                out = qemu(image, initrd, vm_args + " run {} {}".format(
                     suite["name"], test), test_log)
                 sponge_log.write(out)
                 sponge_log.write("\r\n\r\n")
diff --git a/test/hftest/hftest.c b/test/hftest/hftest_common.c
similarity index 71%
rename from test/hftest/hftest.c
rename to test/hftest/hftest_common.c
index ac22d23..871aa54 100644
--- a/test/hftest/hftest.c
+++ b/test/hftest/hftest_common.c
@@ -14,23 +14,20 @@
  * limitations under the License.
  */
 
-#include "hftest.h"
-
-#include <stdalign.h>
-#include <stdint.h>
+#include "hftest_common.h"
 
 #include "hf/arch/std.h"
 #include "hf/arch/vm/power_mgmt.h"
 
-#include "hf/fdt.h"
 #include "hf/memiter.h"
 
-alignas(4096) uint8_t kstack[4096];
+#include "hftest.h"
 
 HFTEST_ENABLE();
 
-extern struct hftest_test hftest_begin[];
-extern struct hftest_test hftest_end[];
+static struct hftest_test hftest_constructed[HFTEST_MAX_TESTS];
+static size_t hftest_count;
+static struct hftest_test *hftest_list;
 
 static struct hftest_context global_context;
 
@@ -39,16 +36,51 @@
 	return &global_context;
 }
 
-static void json(void)
+/**
+ * Adds the given test information to the global list, to be used by
+ * `hftest_use_registered_list`.
+ */
+void hftest_register(struct hftest_test test)
 {
-	struct hftest_test *test;
+	if (hftest_count < HFTEST_MAX_TESTS) {
+		hftest_constructed[hftest_count++] = test;
+	} else {
+		HFTEST_FAIL("Too many tests", true);
+	}
+}
+
+/**
+ * Uses the list of tests registered by `hftest_register(...)` as the ones to
+ * run.
+ */
+void hftest_use_registered_list(void)
+{
+	hftest_list = hftest_constructed;
+}
+
+/**
+ * Uses the given list of tests as the ones to run.
+ */
+void hftest_use_list(struct hftest_test list[], size_t count)
+{
+	hftest_list = list;
+	hftest_count = count;
+}
+
+/**
+ * Writes out a JSON structure describing the available tests.
+ */
+void hftest_json(void)
+{
 	const char *suite = NULL;
+	size_t i;
 	size_t suites_in_image = 0;
 	size_t tests_in_suite = 0;
 
 	HFTEST_LOG("{");
 	HFTEST_LOG("  \"suites\": [");
-	for (test = hftest_begin; test < hftest_end; ++test) {
+	for (i = 0; i < hftest_count; ++i) {
+		struct hftest_test *test = &hftest_list[i];
 		if (test->suite != suite) {
 			/* Close out previously open suite. */
 			if (tests_in_suite) {
@@ -90,6 +122,9 @@
 	HFTEST_LOG("}");
 }
 
+/**
+ * Logs a failure message and shut down.
+ */
 static noreturn void abort(void)
 {
 	HFTEST_LOG("FAIL");
@@ -129,27 +164,19 @@
 	HFTEST_LOG("FINISHED");
 }
 
-static void run(struct memiter *args)
+/**
+ * Runs the given test case.
+ */
+void hftest_run(struct memiter suite_name, struct memiter test_name)
 {
-	struct memiter suite_name;
-	struct memiter test_name;
-	struct hftest_test *test;
+	size_t i;
 	bool found_suite = false;
 	const char *suite = NULL;
 	hftest_test_fn suite_set_up = NULL;
 	hftest_test_fn suite_tear_down = NULL;
 
-	if (!memiter_parse_str(args, &suite_name)) {
-		HFTEST_LOG("Unable to parse test suite.");
-		return;
-	}
-
-	if (!memiter_parse_str(args, &test_name)) {
-		HFTEST_LOG("Unable to parse test.");
-		return;
-	}
-
-	for (test = hftest_begin; test < hftest_end; ++test) {
+	for (i = 0; i < hftest_count; ++i) {
+		struct hftest_test *test = &hftest_list[i];
 		/* Find the test suite. */
 		if (found_suite) {
 			if (test->suite != suite) {
@@ -196,7 +223,10 @@
 	HFTEST_LOG("Unable to find requested tests.");
 }
 
-static void help(void)
+/**
+ * Writes out usage information.
+ */
+void hftest_help(void)
 {
 	HFTEST_LOG("usage:");
 	HFTEST_LOG("");
@@ -215,52 +245,3 @@
 	HFTEST_LOG("");
 	HFTEST_LOG("    Run the named test from the named test suite.");
 }
-
-void kmain(const struct fdt_header *fdt)
-{
-	struct fdt_node n;
-	const char *bootargs;
-	uint32_t bootargs_size;
-	struct memiter bootargs_iter;
-	struct memiter command;
-
-	if (!fdt_root_node(&n, fdt)) {
-		HFTEST_LOG("FDT failed validation.");
-		return;
-	}
-
-	if (!fdt_find_child(&n, "")) {
-		HFTEST_LOG("Unable to find root node in FDT.");
-		return;
-	}
-
-	if (!fdt_find_child(&n, "chosen")) {
-		HFTEST_LOG("Unable to find 'chosen' node in FDT.");
-		return;
-	}
-
-	if (!fdt_read_property(&n, "bootargs", &bootargs, &bootargs_size)) {
-		HFTEST_LOG("Unable to read bootargs.");
-		return;
-	}
-
-	/* Remove null terminator. */
-	memiter_init(&bootargs_iter, bootargs, bootargs_size - 1);
-
-	if (!memiter_parse_str(&bootargs_iter, &command)) {
-		HFTEST_LOG("Unable to parse command.");
-		return;
-	}
-
-	if (memiter_iseq(&command, "json")) {
-		json();
-		return;
-	}
-
-	if (memiter_iseq(&command, "run")) {
-		run(&bootargs_iter);
-		return;
-	}
-
-	help();
-}
diff --git a/test/hftest/inc/hftest_common.h b/test/hftest/inc/hftest_common.h
new file mode 100644
index 0000000..fa35aab
--- /dev/null
+++ b/test/hftest/inc/hftest_common.h
@@ -0,0 +1,26 @@
+/*
+ * 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/memiter.h"
+
+#include "hftest_impl.h"
+
+void hftest_use_registered_list(void);
+void hftest_use_list(struct hftest_test list[], size_t count);
+
+void hftest_json(void);
+void hftest_run(struct memiter suite_name, struct memiter test_name);
+void hftest_help(void);
diff --git a/test/hftest/inc/hftest_impl.h b/test/hftest/inc/hftest_impl.h
index 0470261..c4df298 100644
--- a/test/hftest/inc/hftest_impl.h
+++ b/test/hftest/inc/hftest_impl.h
@@ -20,6 +20,8 @@
 
 #include "hf/arch/std.h"
 
+#define HFTEST_MAX_TESTS 50
+
 /*
  * Log with the HFTEST_LOG_PREFIX and a new line. The zero is added so there is
  * always at least one variadic argument.
@@ -58,6 +60,12 @@
 	hftest_test_fn_##suite_name##_##test_name
 #define HFTEST_SERVICE_FN(service_name) hftest_service_fn_##service_name
 
+#define HFTEST_SET_UP_CONSTRUCTOR(suite_name) hftest_set_up_ctor_##suite_name
+#define HFTEST_TEAR_DOWN_CONSTRUCTOR(suite_name) \
+	hftest_tear_down_ctor_##suite_name
+#define HFTEST_TEST_CONSTRUCTOR(suite_name, test_name) \
+	hftest_test_ctor_##suite_name##_##test_name
+
 /* Register test functions. */
 #define HFTEST_SET_UP(suite_name)                                           \
 	static void HFTEST_SET_UP_FN(suite_name)(void);                     \
@@ -68,6 +76,11 @@
 				.kind = HFTEST_KIND_SET_UP,                 \
 				.fn = HFTEST_SET_UP_FN(suite_name),         \
 	};                                                                  \
+	static void __attribute__((constructor))                            \
+		HFTEST_SET_UP_CONSTRUCTOR(suite_name)(void)                 \
+	{                                                                   \
+		hftest_register(HFTEST_SET_UP_STRUCT(suite_name));          \
+	}                                                                   \
 	static void HFTEST_SET_UP_FN(suite_name)(void)
 
 #define HFTEST_TEAR_DOWN(suite_name)                                           \
@@ -79,18 +92,28 @@
 				.kind = HFTEST_KIND_TEAR_DOWN,                 \
 				.fn = HFTEST_TEAR_DOWN_FN(suite_name),         \
 	};                                                                     \
+	static void __attribute__((constructor))                               \
+		HFTEST_TEAR_DOWN_CONSTRUCTOR(suite_name)(void)                 \
+	{                                                                      \
+		hftest_register(HFTEST_TEAR_DOWN_STRUCT(suite_name));          \
+	}                                                                      \
 	static void HFTEST_TEAR_DOWN_FN(suite_name)(void)
 
-#define HFTEST_TEST(suite_name, test_name)                             \
-	static void HFTEST_TEST_FN(suite_name, test_name)(void);       \
-	const struct hftest_test __attribute__((used)) __attribute__(  \
-		(section(HFTEST_TEST_SECTION(suite_name, test_name)))) \
-		HFTEST_TEST_STRUCT(suite_name, test_name) = {          \
-			.suite = #suite_name,                          \
-			.kind = HFTEST_KIND_TEST,                      \
-			.name = #test_name,                            \
-			.fn = HFTEST_TEST_FN(suite_name, test_name),   \
-	};                                                             \
+#define HFTEST_TEST(suite_name, test_name)                                  \
+	static void HFTEST_TEST_FN(suite_name, test_name)(void);            \
+	const struct hftest_test __attribute__((used)) __attribute__(       \
+		(section(HFTEST_TEST_SECTION(suite_name, test_name))))      \
+		HFTEST_TEST_STRUCT(suite_name, test_name) = {               \
+			.suite = #suite_name,                               \
+			.kind = HFTEST_KIND_TEST,                           \
+			.name = #test_name,                                 \
+			.fn = HFTEST_TEST_FN(suite_name, test_name),        \
+	};                                                                  \
+	static void __attribute__((constructor))                            \
+		HFTEST_TEST_CONSTRUCTOR(suite_name, test_name)(void)        \
+	{                                                                   \
+		hftest_register(HFTEST_TEST_STRUCT(suite_name, test_name)); \
+	}                                                                   \
 	static void HFTEST_TEST_FN(suite_name, test_name)(void)
 
 #define HFTEST_TEST_SERVICE(service_name)                                      \
@@ -264,3 +287,5 @@
 
 #define HFTEST_SERVICE_SEND_BUFFER() hftest_get_context()->send
 #define HFTEST_SERVICE_RECV_BUFFER() hftest_get_context()->recv
+
+void hftest_register(struct hftest_test test);
diff --git a/test/hftest/linux_main.c b/test/hftest/linux_main.c
new file mode 100644
index 0000000..450dc1c
--- /dev/null
+++ b/test/hftest/linux_main.c
@@ -0,0 +1,69 @@
+/*
+ * 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 <stdalign.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "hf/memiter.h"
+
+#include "hftest.h"
+#include "hftest_common.h"
+#include <sys/reboot.h>
+
+void test_main(int argc, const char *argv[])
+{
+	const char *command;
+
+	if (argc < 2) {
+		HFTEST_LOG("Unable to parse command.");
+		return;
+	}
+	command = argv[1];
+
+	hftest_use_registered_list();
+
+	if (strcmp(command, "json") == 0) {
+		hftest_json();
+		return;
+	}
+
+	if (strcmp(command, "run") == 0) {
+		struct memiter suite_name;
+		struct memiter test_name;
+
+		if (argc != 4) {
+			HFTEST_LOG("Unable to parse test.");
+			return;
+		}
+
+		memiter_init(&suite_name, argv[2], strlen(argv[2]));
+		memiter_init(&test_name, argv[3], strlen(argv[3]));
+		hftest_run(suite_name, test_name);
+		return;
+	}
+
+	hftest_help();
+}
+
+int main(int argc, const char *argv[])
+{
+	test_main(argc, argv);
+	reboot(RB_POWER_OFF);
+	return 0;
+}
diff --git a/test/hftest/standalone_main.c b/test/hftest/standalone_main.c
new file mode 100644
index 0000000..b4938e6
--- /dev/null
+++ b/test/hftest/standalone_main.c
@@ -0,0 +1,92 @@
+/*
+ * 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 <stdalign.h>
+#include <stdint.h>
+
+#include "hf/fdt.h"
+#include "hf/memiter.h"
+
+#include "hftest.h"
+#include "hftest_common.h"
+
+alignas(4096) uint8_t kstack[4096];
+
+extern struct hftest_test hftest_begin[];
+extern struct hftest_test hftest_end[];
+
+void kmain(const struct fdt_header *fdt)
+{
+	struct fdt_node n;
+	const char *bootargs;
+	uint32_t bootargs_size;
+	struct memiter bootargs_iter;
+	struct memiter command;
+
+	hftest_use_list(hftest_begin, hftest_end - hftest_begin);
+
+	if (!fdt_root_node(&n, fdt)) {
+		HFTEST_LOG("FDT failed validation.");
+		return;
+	}
+
+	if (!fdt_find_child(&n, "")) {
+		HFTEST_LOG("Unable to find root node in FDT.");
+		return;
+	}
+
+	if (!fdt_find_child(&n, "chosen")) {
+		HFTEST_LOG("Unable to find 'chosen' node in FDT.");
+		return;
+	}
+
+	if (!fdt_read_property(&n, "bootargs", &bootargs, &bootargs_size)) {
+		HFTEST_LOG("Unable to read bootargs.");
+		return;
+	}
+
+	/* Remove null terminator. */
+	memiter_init(&bootargs_iter, bootargs, bootargs_size - 1);
+
+	if (!memiter_parse_str(&bootargs_iter, &command)) {
+		HFTEST_LOG("Unable to parse command.");
+		return;
+	}
+
+	if (memiter_iseq(&command, "json")) {
+		hftest_json();
+		return;
+	}
+
+	if (memiter_iseq(&command, "run")) {
+		struct memiter suite_name;
+		struct memiter test_name;
+
+		if (!memiter_parse_str(&bootargs_iter, &suite_name)) {
+			HFTEST_LOG("Unable to parse test suite.");
+			return;
+		}
+
+		if (!memiter_parse_str(&bootargs_iter, &test_name)) {
+			HFTEST_LOG("Unable to parse test.");
+			return;
+		}
+		hftest_run(suite_name, test_name);
+		return;
+	}
+
+	hftest_help();
+}
diff --git a/test/linux/BUILD.gn b/test/linux/BUILD.gn
new file mode 100644
index 0000000..0c8a9c6
--- /dev/null
+++ b/test/linux/BUILD.gn
@@ -0,0 +1,54 @@
+# 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/image/image.gni")
+
+executable("test_binary") {
+  testonly = true
+  sources = [
+    "linux.c",
+  ]
+  deps = [
+    "//test/hftest:hftest_linux",
+  ]
+  output_name = "test_binary"
+}
+
+linux_initrd("linux_test_initrd") {
+  testonly = true
+  sources = [
+    get_label_info(":test_binary(//build/toolchain:aarch64_linux_clang)",
+                   "root_out_dir") + "/test_binary",
+    get_label_info("//driver:linux", "target_out_dir") + "/linux/hafnium.ko",
+  ]
+  deps = [
+    ":test_binary(//build/toolchain:aarch64_linux_clang)",
+    "//driver:linux",
+  ]
+}
+
+initrd("linux_test") {
+  testonly = true
+
+  primary_vm_prebuilt = "//prebuilts/linux-aarch64/linux/vmlinuz"
+  primary_initrd = ":linux_test_initrd"
+}
+
+group("linux") {
+  testonly = true
+
+  deps = [
+    ":linux_test",
+  ]
+}
diff --git a/test/linux/linux.c b/test/linux/linux.c
new file mode 100644
index 0000000..3300799
--- /dev/null
+++ b/test/linux/linux.c
@@ -0,0 +1,55 @@
+/*
+ * 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 <fcntl.h>
+#include <unistd.h>
+
+#include "hf/dlog.h"
+
+#include "hftest.h"
+#include <sys/syscall.h>
+
+static int finit_module(int fd, const char *param_values, int flags)
+{
+	return syscall(SYS_finit_module, fd, param_values, flags);
+}
+
+static int delete_module(const char *name, int flags)
+{
+	return syscall(SYS_delete_module, name, flags);
+}
+
+static void insmod_hafnium(void)
+{
+	int module_file = open("/hafnium.ko", O_RDONLY);
+	if (module_file < 0) {
+		FAIL("Failed to load Hafnium kernel module from /hafnium.ko");
+		return;
+	}
+	EXPECT_EQ(finit_module(module_file, "", 0), 0);
+	close(module_file);
+}
+
+static void rmmod_hafnium(void)
+{
+	EXPECT_EQ(delete_module("hafnium", 0), 0);
+}
+
+TEST(linux, load_hafnium)
+{
+	insmod_hafnium();
+	rmmod_hafnium();
+}
diff --git a/third_party/BUILD.gn b/third_party/BUILD.gn
index 7a9ad02..52ae5e9 100644
--- a/third_party/BUILD.gn
+++ b/third_party/BUILD.gn
@@ -50,3 +50,47 @@
     ":gtest",
   ]
 }
+
+action("linux_defconfig") {
+  script = "//build/make.py"
+  args = [
+    "--directory",
+    rebase_path("linux"),
+    "--out_file",
+    ".config",
+    "--copy_out_file",
+    rebase_path("${target_out_dir}/.config"),
+    "CC=" + rebase_path("//prebuilts/linux-x64/clang/bin/clang"),
+    "ARCH=arm64",
+    "CROSS_COMPILE=aarch64-linux-gnu-",
+    "-j24",
+    "defconfig",
+    "modules_prepare",
+  ]
+  outputs = [
+    # We don't actually care about this, but GN requires us to have some output.
+    "${target_out_dir}/.config",
+  ]
+}
+
+action("linux") {
+  script = "//build/make.py"
+  args = [
+    "--directory",
+    rebase_path("linux"),
+    "--out_file",
+    "arch/arm64/boot/Image",
+    "--copy_out_file",
+    rebase_path("${target_out_dir}/linux.bin"),
+    "CC=" + rebase_path("//prebuilts/linux-x64/clang/bin/clang"),
+    "ARCH=arm64",
+    "CROSS_COMPILE=aarch64-linux-gnu-",
+    "-j24",
+  ]
+  outputs = [
+    "${target_out_dir}/linux.bin",
+  ]
+  deps = [
+    ":linux_defconfig",
+  ]
+}