diff --git a/apex/Android.bp b/apex/Android.bp index 25cd4807..d12b27be 100644 --- a/apex/Android.bp +++ b/apex/Android.bp @@ -142,11 +142,16 @@ sh_test_host { ], data: [ ":com.android.virt", - "test.com.android.virt.pem", + ":test.com.android.virt.pem", ], test_suites: ["general-tests"], } +filegroup { + name: "test.com.android.virt.pem", + srcs: ["test.com.android.virt.pem"], +} + // custom tool to replace bytes in a file python_binary_host { name: "replace_bytes", diff --git a/microdroid/payload/mk_payload.cc b/microdroid/payload/mk_payload.cc index fd1ce781..6e3f5260 100644 --- a/microdroid/payload/mk_payload.cc +++ b/microdroid/payload/mk_payload.cc @@ -269,24 +269,34 @@ Result MakePayload(const Config& config, const std::string& metadata_file, } int main(int argc, char** argv) { - if (argc != 3) { - std::cerr << "Usage: " << argv[0] << " \n"; + if (argc < 3 || argc > 4) { + std::cerr << "Usage: " << argv[0] << " [--metadata-only] \n"; return 1; } + int arg_index = 1; + bool metadata_only = false; + if (strcmp(argv[arg_index], "--metadata-only") == 0) { + metadata_only = true; + arg_index++; + } - auto config = LoadConfig(argv[1]); + auto config = LoadConfig(argv[arg_index++]); if (!config.ok()) { std::cerr << "bad config: " << config.error() << '\n'; return 1; } - const std::string output_file(argv[2]); - const std::string metadata_file = AppendFileName(output_file, "-metadata"); + const std::string output_file(argv[arg_index++]); + const std::string metadata_file = + metadata_only ? output_file : AppendFileName(output_file, "-metadata"); if (const auto res = MakeMetadata(*config, metadata_file); !res.ok()) { std::cerr << res.error() << '\n'; return 1; } + if (metadata_only) { + return 0; + } if (const auto res = MakePayload(*config, metadata_file, output_file); !res.ok()) { std::cerr << res.error() << '\n'; return 1; diff --git a/tests/Android.bp b/tests/Android.bp index 74d58f54..00628461 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -91,3 +91,11 @@ android_filesystem { ], type: "cpio", } + +genrule { + name: "test-payload-metadata", + tools: ["mk_payload"], + cmd: "$(location mk_payload) --metadata-only $(in) $(out)", + srcs: ["test-payload-metadata-config.json"], + out: ["test-payload-metadata.img"], +} diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp index 10bcbf47..67a0e8d7 100644 --- a/tests/hostside/Android.bp +++ b/tests/hostside/Android.bp @@ -10,6 +10,7 @@ java_test_host { "general-tests", ], libs: [ + "gson-prebuilt-jar", "tradefed", ], static_libs: [ @@ -19,6 +20,31 @@ java_test_host { data: [ ":MicrodroidTestApp", ":microdroid_general_sepolicy.conf", + ":test.com.android.virt.pem", + ":test-payload-metadata", + ], + data_native_bins: [ + "sepolicy-analyze", + // For re-sign test + "avbtool", + "img2simg", + "lpmake", + "lpunpack", + "sign_virt_apex", + "simg2img", + ], + // java_test_host doesn't have data_native_libs but jni_libs can be used to put + // native modules under ./lib directory. + // This works because host tools have rpath (../lib and ./lib). + jni_libs: [ + "libbase", + "libc++", + "libcrypto_utils", + "libcrypto", + "libext4_utils", + "liblog", + "liblp", + "libsparse", + "libz", ], - data_native_bins: ["sepolicy-analyze"], } diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java index e3f19680..e45e524e 100644 --- a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java +++ b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java @@ -43,7 +43,7 @@ import java.util.regex.Pattern; public abstract class VirtualizationTestCaseBase extends BaseHostJUnit4Test { protected static final String TEST_ROOT = "/data/local/tmp/virt/"; - private static final String VIRT_APEX = "/apex/com.android.virt/"; + protected static final String VIRT_APEX = "/apex/com.android.virt/"; private static final int TEST_VM_ADB_PORT = 8000; private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT; private static final String INSTANCE_IMG = "instance.img"; diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java index 10b90d35..d0bc91aa 100644 --- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java +++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java @@ -16,12 +16,14 @@ package android.virt.test; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.result.TestDescription; @@ -33,14 +35,23 @@ import com.android.tradefed.util.CommandStatus; import com.android.tradefed.util.FileUtil; import com.android.tradefed.util.RunUtil; +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @RunWith(DeviceJUnit4ClassRunner.class) public class MicrodroidTestCase extends VirtualizationTestCaseBase { @@ -77,6 +88,11 @@ public class MicrodroidTestCase extends VirtualizationTestCaseBase { return 0; } + private boolean isProtectedVmSupported() throws DeviceNotAvailableException { + return getDevice().getBooleanProperty("ro.boot.hypervisor.protected_vm.supported", + false); + } + @Test public void testCreateVmRequiresPermission() throws Exception { // Revoke the MANAGE_VIRTUAL_MACHINE permission for the test app @@ -98,6 +114,195 @@ public class MicrodroidTestCase extends VirtualizationTestCaseBase { .contains("android.permission.MANAGE_VIRTUAL_MACHINE permission")); } + // Helper classes for (de)serialization of VM raw configs + static class VmRawConfig { + String bootloader; + List disks; + int memory_mib; + @SerializedName("protected") + boolean isProtected; + } + + static class Disk { + List partitions; + boolean writable; + public void addPartition(String label, String path) { + if (partitions == null) { + partitions = new ArrayList(); + } + Partition partition = new Partition(); + partition.label = label; + partition.path = path; + partitions.add(partition); + } + } + + static class Partition { + String label; + String path; + boolean writable; + } + + private String getPathForPackage(String packageName) + throws DeviceNotAvailableException { + CommandRunner android = new CommandRunner(getDevice()); + String pathLine = android.run("pm", "path", packageName); + assertTrue("package not found", pathLine.startsWith("package:")); + return pathLine.substring("package:".length()); + } + + private void resignVirtApex(File virtApexDir, File signingKey) { + File signVirtApex = findTestFile("sign_virt_apex"); + + RunUtil runUtil = new RunUtil(); + // Set the parent dir on the PATH (e.g. /bin) + String separator = System.getProperty("path.separator"); + String path = signVirtApex.getParentFile().getPath() + separator + System.getenv("PATH"); + runUtil.setEnvVariable("PATH", path); + + String resignCommand = String.format("sign_virt_apex %s %s", + signingKey.getPath(), + virtApexDir.getPath()); + CommandResult result = runUtil.runTimedCmd( + 20 * 1000, + "/bin/bash", + "-c", + resignCommand); + String out = result.getStdout(); + String err = result.getStderr(); + assertEquals( + "resigning the Virt APEX failed:\n\tout: " + out + "\n\terr: " + err + "\n", + CommandStatus.SUCCESS, result.getStatus()); + } + + private String runMicrodroidWithResignedImages(boolean isProtected, boolean daemonize, + String consolePath) throws DeviceNotAvailableException, IOException { + CommandRunner android = new CommandRunner(getDevice()); + + File virtApexDir = FileUtil.createTempDir("virt_apex"); + + // Pull the virt apex's etc/ directory (which contains images and microdroid.json) + File virtApexEtcDir = new File(virtApexDir, "etc"); + // We need only etc/ directory for images + assertTrue(virtApexEtcDir.mkdirs()); + assertTrue(getDevice().pullDir(VIRT_APEX + "etc", virtApexEtcDir)); + + File testKey = findTestFile("test.com.android.virt.pem"); + resignVirtApex(virtApexDir, testKey); + + // Push back re-signed virt APEX contents and updated microdroid.json + getDevice().pushDir(virtApexDir, TEST_ROOT); + + // Create the idsig file for the APK + final String apkPath = getPathForPackage(PACKAGE_NAME); + final String idSigPath = TEST_ROOT + "idsig"; + android.run(VIRT_APEX + "bin/vm", "create-idsig", apkPath, idSigPath); + + // Create the instance image for the VM + final String instanceImgPath = TEST_ROOT + "instance.img"; + android.run(VIRT_APEX + "bin/vm", "create-partition", "--type instance", + instanceImgPath, Integer.toString(10 * 1024 * 1024)); + + // payload-metadata is prepared on host with the two APEXes and APK + final String payloadMetadataPath = TEST_ROOT + "payload-metadata.img"; + getDevice().pushFile(findTestFile("test-payload-metadata.img"), payloadMetadataPath); + + // Since Java APP can't start a VM with a custom image, here, we start a VM using `vm run` + // command with a VM Raw config which is equiv. to what virtualizationservice creates with + // a VM App config. + // + // 1. use etc/microdroid.json as base + // 2. add partitions: bootconfig, vbmeta, instance image + // 3. add a payload image disk with + // - payload-metadata + // - apexes + // - test apk + // - its idsig + + // Load etc/microdroid.json + Gson gson = new Gson(); + File microdroidConfigFile = new File(virtApexEtcDir, "microdroid.json"); + VmRawConfig config = gson.fromJson(new FileReader(microdroidConfigFile), + VmRawConfig.class); + + // Replace paths so that the config uses re-signed images from TEST_ROOT + config.bootloader = config.bootloader.replace(VIRT_APEX, TEST_ROOT); + for (Disk disk : config.disks) { + for (Partition part : disk.partitions) { + part.path = part.path.replace(VIRT_APEX, TEST_ROOT); + } + } + + // Add partitions to the second disk + Disk secondDisk = config.disks.get(1); + secondDisk.addPartition("vbmeta", + TEST_ROOT + "etc/fs/microdroid_vbmeta_bootconfig.img"); + secondDisk.addPartition("bootconfig", + TEST_ROOT + "etc/microdroid_bootconfig.full_debuggable"); + secondDisk.addPartition("vm-instance", instanceImgPath); + + // Add payload image disk with partitions: + // - payload-metadata + // - apexes: com.android.os.statsd, com.android.adbd + // - apk and idsig + Disk payloadDisk = new Disk(); + payloadDisk.addPartition("payload-metadata", payloadMetadataPath); + String[] apexes = {"com.android.os.statsd", "com.android.adbd"}; + for (int i = 0; i < apexes.length; i++) { + String apexPath = getPathForPackage(apexes[i]); + String filename = apexes[i] + ".apex"; + File localApexFile = new File(virtApexDir, filename); + String remoteApexFile = TEST_ROOT + filename; + // Since `adb shell vm` can't access apex_data_file, we `adb pull/push` apex files. + getDevice().pullFile(apexPath, localApexFile); + getDevice().pushFile(localApexFile, remoteApexFile); + payloadDisk.addPartition("microdroid-apex-" + i, remoteApexFile); + } + payloadDisk.addPartition("microdroid-apk", apkPath); + payloadDisk.addPartition("microdroid-apk-idsig", idSigPath); + config.disks.add(payloadDisk); + + config.isProtected = isProtected; + + // Write updated raw config + final String configPath = TEST_ROOT + "raw_config.json"; + getDevice().pushString(gson.toJson(config), configPath); + + final String logPath = TEST_ROOT + "log"; + final String ret = android.runWithTimeout( + 60 * 1000, + VIRT_APEX + "bin/vm run", + daemonize ? "--daemonize" : "", + (consolePath != null) ? "--console " + consolePath : "", + "--log " + logPath, + configPath); + Pattern pattern = Pattern.compile("with CID (\\d+)"); + Matcher matcher = pattern.matcher(ret); + assertTrue(matcher.find()); + return matcher.group(1); + } + + @Test + public void testBootFailsWhenProtectedVmStartsWithImagesSignedWithDifferentKey() + throws Exception { + assumeTrue(isProtectedVmSupported()); + String consolePath = TEST_ROOT + "console"; + // Run VM without --daemonize. It will shut down due to boot failure. + runMicrodroidWithResignedImages(/*protected=*/true, /*daemonize=*/false, consolePath); + assertThat(getDevice().pullFileContents(consolePath), + containsString("pvmfw boot failed")); + } + + @Test + public void testBootSucceedsWhenNonProtectedVmStartsWithImagesSignedWithDifferentKey() + throws Exception { + String cid = runMicrodroidWithResignedImages(/*protected=*/false, + /*daemonize=*/true, /*consolePath=*/null); + // Adb connection to the microdroid means that boot succeeded. + adbConnectToMicrodroid(getDevice(), cid); + shutdownMicrodroid(getDevice(), cid); + } + @Test public void testMicrodroidBoots() throws Exception { final String configPath = "assets/vm_config.json"; // path inside the APK diff --git a/tests/test-payload-metadata-config.json b/tests/test-payload-metadata-config.json new file mode 100644 index 00000000..3c56e5fa --- /dev/null +++ b/tests/test-payload-metadata-config.json @@ -0,0 +1,19 @@ +{ + "_comment": "This file is to create a payload-metadata partition for payload.img which is for MicrodroidTestApp to run with assets/vm_config.json", + "apexes": [ + { + "name": "com.android.os.statsd", + "path": "" + }, + { + "name": "com.android.adbd", + "path": "" + } + ], + "apk": { + "name": "microdroid-apk", + "path": "", + "idsig_path": "" + }, + "payload_config_path": "/mnt/apk/assets/vm_config.json" +} \ No newline at end of file