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,
)
}