Merge "Common helper to pick the right VM protection mode"
diff --git a/apex/Android.bp b/apex/Android.bp
index 25cd480..d12b27b 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -142,11 +142,16 @@
     ],
     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/authfs/src/fusefs/mount.rs b/authfs/src/fusefs/mount.rs
index e7f8c94..294c6b1 100644
--- a/authfs/src/fusefs/mount.rs
+++ b/authfs/src/fusefs/mount.rs
@@ -53,8 +53,13 @@
         mount_options.push(MountOption::Extra(value));
     }
 
-    fuse::mount(mountpoint, "authfs", libc::MS_NOSUID | libc::MS_NODEV, &mount_options)
-        .expect("Failed to mount fuse");
+    fuse::mount(
+        mountpoint,
+        "authfs",
+        libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC,
+        &mount_options,
+    )
+    .expect("Failed to mount fuse");
 
     fuse::worker::start_message_loop(dev_fuse, MAX_WRITE_BYTES, MAX_READ_BYTES, authfs)
 }
diff --git a/microdroid/payload/mk_payload.cc b/microdroid/payload/mk_payload.cc
index fd1ce78..6e3f526 100644
--- a/microdroid/payload/mk_payload.cc
+++ b/microdroid/payload/mk_payload.cc
@@ -269,24 +269,34 @@
 }
 
 int main(int argc, char** argv) {
-    if (argc != 3) {
-        std::cerr << "Usage: " << argv[0] << " <config> <output>\n";
+    if (argc < 3 || argc > 4) {
+        std::cerr << "Usage: " << argv[0] << " [--metadata-only] <config> <output>\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 74d58f5..0062846 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -91,3 +91,11 @@
     ],
     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 10bcbf4..67a0e8d 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -10,6 +10,7 @@
         "general-tests",
     ],
     libs: [
+        "gson-prebuilt-jar",
         "tradefed",
     ],
     static_libs: [
@@ -19,6 +20,31 @@
     data: [
         ":MicrodroidTestApp",
         ":microdroid_general_sepolicy.conf",
+        ":test.com.android.virt.pem",
+        ":test-payload-metadata",
     ],
-    data_native_bins: ["sepolicy-analyze"],
+    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",
+    ],
 }
diff --git a/tests/hostside/helper/Android.bp b/tests/hostside/helper/Android.bp
index aa748ab..4ca0bf0 100644
--- a/tests/hostside/helper/Android.bp
+++ b/tests/hostside/helper/Android.bp
@@ -2,12 +2,9 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-java_test_helper_library {
+java_library_host {
     name: "VirtualizationTestHelper",
-    host_supported: true,
-    device_supported: false,
     srcs: ["java/**/*.java"],
-    test_suites: ["general-tests"],
     libs: [
         "tradefed",
         "compatibility-tradefed",
diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
index e3f1968..0f6204c 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 @@
 
 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";
@@ -194,6 +194,22 @@
         }
     }
 
+    public String getPathForPackage(String packageName)
+            throws DeviceNotAvailableException {
+        return getPathForPackage(getDevice(), packageName);
+    }
+
+    // Get the path to the installed apk. Note that
+    // getDevice().getAppPackageInfo(...).getCodePath() doesn't work due to the incorrect
+    // parsing of the "=" character. (b/190975227). So we use the `pm path` command directly.
+    private static String getPathForPackage(ITestDevice device, String packageName)
+            throws DeviceNotAvailableException {
+        CommandRunner android = new CommandRunner(device);
+        String pathLine = android.run("pm", "path", packageName);
+        assertTrue("package not found", pathLine.startsWith("package:"));
+        return pathLine.substring("package:".length());
+    }
+
     public static String startMicrodroid(
             ITestDevice androidDevice,
             IBuildInfo buildInfo,
@@ -247,13 +263,8 @@
             androidDevice.installPackage(apkFile, /* reinstall */ true);
         }
 
-        // Get the path to the installed apk. Note that
-        // getDevice().getAppPackageInfo(...).getCodePath() doesn't work due to the incorrect
-        // parsing of the "=" character. (b/190975227). So we use the `pm path` command directly.
         if (apkPath == null) {
-            apkPath = android.run("pm", "path", packageName);
-            assertTrue(apkPath.startsWith("package:"));
-            apkPath = apkPath.substring("package:".length());
+            apkPath = getPathForPackage(androidDevice, packageName);
         }
 
         android.run("mkdir", "-p", TEST_ROOT);
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 10b90d3..e65459a 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.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 @@
         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,187 @@
                 .contains("android.permission.MANAGE_VIRTUAL_MACHINE permission"));
     }
 
+    // Helper classes for (de)serialization of VM raw configs
+    static class VmRawConfig {
+        String bootloader;
+        List<Disk> disks;
+        int memory_mib;
+        @SerializedName("protected")
+        boolean isProtected;
+    }
+
+    static class Disk {
+        List<Partition> partitions;
+        boolean writable;
+        public void addPartition(String label, String path) {
+            if (partitions == null) {
+                partitions = new ArrayList<Partition>();
+            }
+            Partition partition = new Partition();
+            partition.label = label;
+            partition.path = path;
+            partitions.add(partition);
+        }
+    }
+
+    static class Partition {
+        String label;
+        String path;
+        boolean writable;
+    }
+
+    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. <workdir>/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 0000000..3c56e5f
--- /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
diff --git a/vm/src/create_idsig.rs b/vm/src/create_idsig.rs
new file mode 100644
index 0000000..a0d64d5
--- /dev/null
+++ b/vm/src/create_idsig.rs
@@ -0,0 +1,44 @@
+// Copyright 2022, The Android Open Source Project
+//
+// 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
+//
+//     http://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.
+
+//! Command to create or update an idsig for APK
+
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::IVirtualizationService;
+use android_system_virtualizationservice::binder::{ParcelFileDescriptor, Strong};
+use anyhow::{Context, Error};
+use std::fs::{File, OpenOptions};
+use std::path::Path;
+
+/// Creates or update the idsig file by digesting the input APK file.
+pub fn command_create_idsig(
+    service: Strong<dyn IVirtualizationService>,
+    apk: &Path,
+    idsig: &Path,
+) -> Result<(), Error> {
+    let apk_file = File::open(apk).with_context(|| format!("Failed to open {:?}", apk))?;
+    let idsig_file = OpenOptions::new()
+        .create(true)
+        .truncate(true)
+        .read(true)
+        .write(true)
+        .open(idsig)
+        .with_context(|| format!("Failed to create/open {:?}", idsig))?;
+    service
+        .createOrUpdateIdsigFile(
+            &ParcelFileDescriptor::new(apk_file),
+            &ParcelFileDescriptor::new(idsig_file),
+        )
+        .with_context(|| format!("Failed to create/update idsig for {:?}", apk))?;
+    Ok(())
+}
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 2cbae3e..80ea9be 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -14,6 +14,7 @@
 
 //! Android VM control tool.
 
+mod create_idsig;
 mod create_partition;
 mod run;
 mod sync;
@@ -24,6 +25,7 @@
 };
 use android_system_virtualizationservice::binder::{wait_for_interface, ProcessState, Strong};
 use anyhow::{Context, Error};
+use create_idsig::command_create_idsig;
 use create_partition::command_create_partition;
 use run::{command_run, command_run_app};
 use rustutils::system_properties;
@@ -119,6 +121,10 @@
         /// Path to file for VM console output.
         #[structopt(long)]
         console: Option<PathBuf>,
+
+        /// Path to file for VM log output.
+        #[structopt(long)]
+        log: Option<PathBuf>,
     },
     /// Stop a virtual machine running in the background
     Stop {
@@ -142,6 +148,15 @@
         #[structopt(short="t", long="type", default_value="raw", parse(try_from_str=parse_partition_type))]
         partition_type: PartitionType,
     },
+    /// Creates or update the idsig file by digesting the input APK file.
+    CreateIdsig {
+        /// Path to VM Payload APK
+        #[structopt(parse(from_os_str))]
+        apk: PathBuf,
+        /// Path to idsig of the APK
+        #[structopt(parse(from_os_str))]
+        path: PathBuf,
+    },
 }
 
 fn parse_debug_level(s: &str) -> Result<DebugLevel, String> {
@@ -202,12 +217,13 @@
             cpu_affinity,
             &extra_idsigs,
         ),
-        Opt::Run { config, daemonize, cpus, cpu_affinity, console } => {
+        Opt::Run { config, daemonize, cpus, cpu_affinity, console, log } => {
             command_run(
                 service,
                 &config,
                 daemonize,
                 console.as_deref(),
+                log.as_deref(),
                 /* mem */ None,
                 cpus,
                 cpu_affinity,
@@ -219,6 +235,7 @@
         Opt::CreatePartition { path, size, partition_type } => {
             command_create_partition(service, &path, size, partition_type)
         }
+        Opt::CreateIdsig { apk, path } => command_create_idsig(service, &apk, &path),
     }
 }
 
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 6a0fc15..ef38d7d 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -117,11 +117,13 @@
 }
 
 /// Run a VM from the given configuration file.
+#[allow(clippy::too_many_arguments)]
 pub fn command_run(
     service: Strong<dyn IVirtualizationService>,
     config_path: &Path,
     daemonize: bool,
     console_path: Option<&Path>,
+    log_path: Option<&Path>,
     mem: Option<u32>,
     cpus: Option<u32>,
     cpu_affinity: Option<String>,
@@ -142,7 +144,7 @@
         &format!("{:?}", config_path),
         daemonize,
         console_path,
-        None,
+        log_path,
     )
 }