blob: c87ed804d5a251d3166b329c9546fb1639e4c7e4 [file] [log] [blame]
/*
* Copyright (C) 2021 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.
*/
package com.android.microdroid.test;
import static com.android.microdroid.test.host.CommandResultSubject.command_results;
import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static java.util.stream.Collectors.toList;
import android.cts.statsdatom.lib.ConfigUtils;
import android.cts.statsdatom.lib.ReportUtils;
import com.android.compatibility.common.util.CddTest;
import com.android.microdroid.test.common.ProcessUtil;
import com.android.microdroid.test.host.CommandRunner;
import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
import com.android.os.AtomsProto;
import com.android.os.StatsLog;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.TestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.xml.AbstractXmlParser;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@RunWith(DeviceJUnit4ClassRunner.class)
public class MicrodroidHostTests extends MicrodroidHostTestCaseBase {
private static final String APK_NAME = "MicrodroidTestApp.apk";
private static final String PACKAGE_NAME = "com.android.microdroid.test";
private static final String SHELL_PACKAGE_NAME = "com.android.shell";
private static final String VIRT_APEX = "/apex/com.android.virt/";
private static final int MIN_MEM_ARM64 = 145;
private static final int MIN_MEM_X86_64 = 196;
// Number of vCPUs for testing purpose
private static final int NUM_VCPUS = 3;
private static final int BOOT_COMPLETE_TIMEOUT = 30000; // 30 seconds
private static final Pattern sCIDPattern = Pattern.compile("with CID (\\d+)");
private static class VmInfo {
final Process mProcess;
final String mCid;
VmInfo(Process process, String cid) {
mProcess = process;
mCid = cid;
}
}
@Rule public TestLogData mTestLogs = new TestLogData();
@Rule public TestName mTestName = new TestName();
@Rule public TestMetrics mMetrics = new TestMetrics();
private String mMetricPrefix;
private ITestDevice mMicrodroidDevice;
private int minMemorySize() throws DeviceNotAvailableException {
CommandRunner android = new CommandRunner(getDevice());
String abi = android.run("getprop", "ro.product.cpu.abi");
assertThat(abi).isNotEmpty();
if (abi.startsWith("arm64")) {
return MIN_MEM_ARM64;
} else if (abi.startsWith("x86_64")) {
return MIN_MEM_X86_64;
}
throw new AssertionError("Unsupported ABI: " + abi);
}
private static JSONObject newPartition(String label, String path) {
return new JSONObject(Map.of("label", label, "path", path));
}
private void createPayloadMetadata(List<ActiveApexInfo> apexes, File payloadMetadata)
throws Exception {
// mk_payload's config
File configFile = new File(payloadMetadata.getParentFile(), "payload_config.json");
JSONObject config = new JSONObject();
config.put(
"apk",
new JSONObject(Map.of("name", "microdroid-apk", "path", "", "idsig_path", "")));
config.put("payload_config_path", "/mnt/apk/assets/vm_config.json");
config.put(
"apexes",
new JSONArray(
apexes.stream()
.map(apex -> new JSONObject(Map.of("name", apex.name, "path", "")))
.collect(toList())));
FileUtil.writeToFile(config.toString(), configFile);
RunUtil runUtil = new RunUtil();
String command =
String.join(
" ",
findTestFile("mk_payload").getAbsolutePath(),
"--metadata-only",
configFile.getAbsolutePath(),
payloadMetadata.getAbsolutePath());
// mk_payload should run fast enough
CommandResult result = runUtil.runTimedCmd(5000, "/bin/bash", "-c", command);
String out = result.getStdout();
String err = result.getStderr();
assertWithMessage(
"creating payload metadata failed:\n\tout: "
+ out
+ "\n\terr: "
+ err
+ "\n")
.about(command_results())
.that(result)
.isSuccess();
}
private void resignVirtApex(
File virtApexDir,
File signingKey,
Map<String, File> keyOverrides,
boolean updateBootconfigs) {
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);
List<String> command = new ArrayList<>();
command.add(signVirtApex.getAbsolutePath());
if (!updateBootconfigs) {
command.add("--do_not_update_bootconfigs");
}
keyOverrides.forEach(
(filename, keyFile) ->
command.add("--key_override " + filename + "=" + keyFile.getPath()));
command.add(signingKey.getPath());
command.add(virtApexDir.getPath());
CommandResult result =
runUtil.runTimedCmd(
// sign_virt_apex is so slow on CI server that this often times
// out. Until we can make it fast, use 50s for timeout
50 * 1000, "/bin/bash", "-c", String.join(" ", command));
String out = result.getStdout();
String err = result.getStderr();
assertWithMessage(
"resigning the Virt APEX failed:\n\tout: " + out + "\n\terr: " + err + "\n")
.about(command_results())
.that(result)
.isSuccess();
}
private static <T> void assertThatEventually(
long timeoutMillis, Callable<T> callable, org.hamcrest.Matcher<T> matcher)
throws Exception {
long start = System.currentTimeMillis();
while ((System.currentTimeMillis() - start < timeoutMillis)
&& !matcher.matches(callable.call())) {
Thread.sleep(500);
}
assertThat(callable.call(), matcher);
}
static class ActiveApexInfo {
public String name;
public String path;
public boolean provideSharedApexLibs;
ActiveApexInfo(String name, String path, boolean provideSharedApexLibs) {
this.name = name;
this.path = path;
this.provideSharedApexLibs = provideSharedApexLibs;
}
}
static class ActiveApexInfoList {
private List<ActiveApexInfo> mList;
ActiveApexInfoList(List<ActiveApexInfo> list) {
this.mList = list;
}
ActiveApexInfo get(String apexName) {
return mList.stream()
.filter(info -> apexName.equals(info.name))
.findFirst()
.orElse(null);
}
List<ActiveApexInfo> getSharedLibApexes() {
return mList.stream().filter(info -> info.provideSharedApexLibs).collect(toList());
}
}
private ActiveApexInfoList getActiveApexInfoList() throws Exception {
String apexInfoListXml = getDevice().pullFileContents("/apex/apex-info-list.xml");
List<ActiveApexInfo> list = new ArrayList<>();
new AbstractXmlParser() {
@Override
protected DefaultHandler createXmlHandler() {
return new DefaultHandler() {
@Override
public void startElement(
String uri, String localName, String qName, Attributes attributes) {
if (localName.equals("apex-info")
&& attributes.getValue("isActive").equals("true")) {
String name = attributes.getValue("moduleName");
String path = attributes.getValue("modulePath");
String sharedApex = attributes.getValue("provideSharedApexLibs");
list.add(new ActiveApexInfo(name, path, "true".equals(sharedApex)));
}
}
};
}
}.parse(new ByteArrayInputStream(apexInfoListXml.getBytes()));
return new ActiveApexInfoList(list);
}
private VmInfo runMicrodroidWithResignedImages(
File key,
Map<String, File> keyOverrides,
boolean isProtected,
boolean updateBootconfigs)
throws Exception {
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
assertWithMessage("Failed to mkdir " + virtApexEtcDir)
.that(virtApexEtcDir.mkdirs()).isTrue();
assertWithMessage("Failed to pull " + VIRT_APEX + "etc")
.that(getDevice().pullDir(VIRT_APEX + "etc", virtApexEtcDir)).isTrue();
resignVirtApex(virtApexDir, key, keyOverrides, updateBootconfigs);
// 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 created on device
final String payloadMetadataPath = TEST_ROOT + "payload-metadata.img";
// Load /apex/apex-info-list.xml to get paths to APEXes required for the VM.
ActiveApexInfoList list = getActiveApexInfoList();
// 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
File microdroidConfigFile = new File(virtApexEtcDir, "microdroid.json");
JSONObject config = new JSONObject(FileUtil.readStringFromFile(microdroidConfigFile));
// Replace paths so that the config uses re-signed images from TEST_ROOT
config.put("kernel", config.getString("kernel").replace(VIRT_APEX, TEST_ROOT));
JSONArray disks = config.getJSONArray("disks");
for (int diskIndex = 0; diskIndex < disks.length(); diskIndex++) {
JSONObject disk = disks.getJSONObject(diskIndex);
JSONArray partitions = disk.getJSONArray("partitions");
for (int partIndex = 0; partIndex < partitions.length(); partIndex++) {
JSONObject part = partitions.getJSONObject(partIndex);
part.put("path", part.getString("path").replace(VIRT_APEX, TEST_ROOT));
}
}
// Add partitions to the second disk
final String initrdPath = TEST_ROOT + "etc/microdroid_initrd_debuggable.img";
config.put("initrd", initrdPath);
// Add instance image as a partition in disks[1]
disks.put(new JSONObject()
.put("writable", true)
.put("partitions",
new JSONArray().put(newPartition("vm-instance", instanceImgPath))));
// Add payload image disk with partitions:
// - payload-metadata
// - apexes: com.android.os.statsd, com.android.adbd, [sharedlib apex](optional)
// - apk and idsig
List<ActiveApexInfo> apexesForVm = new ArrayList<>();
apexesForVm.add(list.get("com.android.os.statsd"));
apexesForVm.add(list.get("com.android.adbd"));
apexesForVm.addAll(list.getSharedLibApexes());
final JSONArray partitions = new JSONArray();
partitions.put(newPartition("payload-metadata", payloadMetadataPath));
for (ActiveApexInfo apex : apexesForVm) {
partitions.put(newPartition(apex.name, apex.path));
}
partitions
.put(newPartition("microdroid-apk", apkPath))
.put(newPartition("microdroid-apk-idsig", idSigPath));
disks.put(new JSONObject().put("writable", false).put("partitions", partitions));
final File localPayloadMetadata = new File(virtApexDir, "payload-metadata.img");
createPayloadMetadata(apexesForVm, localPayloadMetadata);
getDevice().pushFile(localPayloadMetadata, payloadMetadataPath);
config.put("protected", isProtected);
// Write updated raw config
final String configPath = TEST_ROOT + "raw_config.json";
getDevice().pushString(config.toString(), configPath);
List<String> args =
Arrays.asList(
"adb",
"-s",
getDevice().getSerialNumber(),
"shell",
VIRT_APEX + "bin/vm run",
"--console " + CONSOLE_PATH,
"--log " + LOG_PATH,
configPath);
PipedInputStream pis = new PipedInputStream();
Process process = RunUtil.getDefault().runCmdInBackground(args, new PipedOutputStream(pis));
return new VmInfo(process, extractCidFrom(pis));
}
private static Optional<String> tryExtractCidFrom(String str) {
Matcher matcher = sCIDPattern.matcher(str);
if (matcher.find()) {
return Optional.of(matcher.group(1));
}
return Optional.empty();
}
private static String extractCidFrom(InputStream input) throws IOException {
String cid = null;
String line;
try (BufferedReader out = new BufferedReader(new InputStreamReader(input))) {
while ((line = out.readLine()) != null) {
CLog.i("VM output: " + line);
Optional<String> result = tryExtractCidFrom(line);
if (result.isPresent()) {
cid = result.get();
break;
}
}
}
assertWithMessage("The output does not contain the expected pattern for CID.")
.that(cid)
.isNotNull();
return cid;
}
@Test
@CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-6"})
public void protectedVmRunsPvmfw() throws Exception {
// Arrange
boolean protectedVm = true;
assumeTrue(
"Skip if protected VMs are not supported",
getAndroidDevice().supportsMicrodroid(protectedVm));
final String configPath = "assets/vm_config_apex.json";
// Act
mMicrodroidDevice =
MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
.debugLevel("full")
.memoryMib(minMemorySize())
.numCpus(NUM_VCPUS)
.protectedVm(protectedVm)
.build(getAndroidDevice());
// Assert
mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
String consoleLog = getDevice().pullFileContents(CONSOLE_PATH);
assertWithMessage("Failed to verify that pvmfw started")
.that(consoleLog)
.contains("pVM firmware");
assertWithMessage("pvmfw failed to start kernel")
.that(consoleLog)
.contains("Starting payload...");
// TODO(b/260994818): Investigate the feasibility of checking DeathReason.
}
@Test
@CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-6"})
public void protectedVmWithImageSignedWithDifferentKeyRunsPvmfw() throws Exception {
// Arrange
boolean protectedVm = true;
assumeTrue(
"Skip if protected VMs are not supported",
getAndroidDevice().supportsMicrodroid(protectedVm));
File key = findTestFile("test.com.android.virt.pem");
// Act
VmInfo vmInfo =
runMicrodroidWithResignedImages(
key, /*keyOverrides=*/ Map.of(), protectedVm, /*updateBootconfigs=*/ true);
// Assert
vmInfo.mProcess.waitFor(5L, TimeUnit.SECONDS);
String consoleLog = getDevice().pullFileContents(CONSOLE_PATH);
assertWithMessage("pvmfw should start").that(consoleLog).contains("pVM firmware");
// TODO(b/256148034): Asserts that pvmfw run fails when this verification is implemented.
// Also rename the test.
vmInfo.mProcess.destroy();
}
@Test
@CddTest(requirements = {"9.17/C-2-2", "9.17/C-2-6"})
public void testBootSucceedsWhenNonProtectedVmStartsWithImagesSignedWithDifferentKey()
throws Exception {
File key = findTestFile("test.com.android.virt.pem");
Map<String, File> keyOverrides = Map.of();
VmInfo vmInfo =
runMicrodroidWithResignedImages(
key, keyOverrides, /*isProtected=*/ false, /*updateBootconfigs=*/ true);
// Device online means that boot must have succeeded.
adbConnectToMicrodroid(getDevice(), vmInfo.mCid);
vmInfo.mProcess.destroy();
}
@Test
@CddTest(requirements = {"9.17/C-2-2", "9.17/C-2-6"})
public void testBootFailsWhenVbMetaDigestDoesNotMatchBootconfig() throws Exception {
// Sign everything with key1 except vbmeta
File key = findTestFile("test.com.android.virt.pem");
// To be able to stop it, it should be a daemon.
VmInfo vmInfo =
runMicrodroidWithResignedImages(
key, Map.of(), /*isProtected=*/ false, /*updateBootconfigs=*/ false);
// Wait so that init can print errors to console (time in cuttlefish >> in real device)
assertThatEventually(
100000,
() -> getDevice().pullFileContents(CONSOLE_PATH),
containsString("init: [libfs_avb] Failed to verify vbmeta digest"));
vmInfo.mProcess.destroy();
}
private boolean isTombstoneGenerated(String configPath, String... crashCommand)
throws Exception {
// Note this test relies on logcat values being printed by tombstone_transmit on
// and the reeceiver on host (virtualization_service)
mMicrodroidDevice =
MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
.debugLevel("full")
.memoryMib(minMemorySize())
.numCpus(NUM_VCPUS)
.build(getAndroidDevice());
mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
mMicrodroidDevice.enableAdbRoot();
CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
microdroid.run(crashCommand);
// check until microdroid is shut down
CommandRunner android = new CommandRunner(getDevice());
// TODO: improve crosvm exit check. b/258848245
android.runWithTimeout(
15000,
"logcat",
"-m",
"1",
"-e",
"'virtualizationmanager::crosvm.*exited with status exit status:'");
// Check that tombstone is received (from host logcat)
String ramdumpRegex =
"Received [0-9]+ bytes from guest & wrote to tombstone file|"
+ "Ramdump \"[^ ]+/ramdump\" sent to tombstoned";
String result =
tryRunOnHost(
"timeout",
"10s",
"adb",
"-s",
getDevice().getSerialNumber(),
"logcat",
"-m",
"1",
"-e",
ramdumpRegex);
return !result.trim().isEmpty();
}
@Test
public void testTombstonesAreGeneratedUponUserspaceCrash() throws Exception {
assertThat(
isTombstoneGenerated(
"assets/vm_config_crash.json",
"kill",
"-SIGSEGV",
"$(pidof microdroid_launcher)"))
.isTrue();
}
@Test
public void testTombstonesAreNotGeneratedIfNotExportedUponUserspaceCrash() throws Exception {
assertThat(
isTombstoneGenerated(
"assets/vm_config_crash_no_tombstone.json",
"kill",
"-SIGSEGV",
"$(pidof microdroid_launcher)"))
.isFalse();
}
@Test
@Ignore("b/243630590: Temporal workaround until lab devices has flashed new DPM")
public void testTombstonesAreGeneratedUponKernelCrash() throws Exception {
assumeFalse("Cuttlefish is not supported", isCuttlefish());
assertThat(
isTombstoneGenerated(
"assets/vm_config_crash.json",
"echo",
"c",
">",
"/proc/sysrq-trigger"))
.isTrue();
}
@Test
public void testTelemetryPushedAtoms() throws Exception {
// Reset statsd config and report before the test
ConfigUtils.removeConfig(getDevice());
ReportUtils.clearReports(getDevice());
// Setup statsd config
int[] atomIds = {
AtomsProto.Atom.VM_CREATION_REQUESTED_FIELD_NUMBER,
AtomsProto.Atom.VM_BOOTED_FIELD_NUMBER,
AtomsProto.Atom.VM_EXITED_FIELD_NUMBER,
};
ConfigUtils.uploadConfigForPushedAtoms(getDevice(), PACKAGE_NAME, atomIds);
// Create VM with microdroid
TestDevice device = getAndroidDevice();
final String configPath = "assets/vm_config_apex.json"; // path inside the APK
ITestDevice microdroid =
MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
.debugLevel("full")
.memoryMib(minMemorySize())
.numCpus(NUM_VCPUS)
.build(device);
microdroid.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
device.shutdownMicrodroid(microdroid);
List<StatsLog.EventMetricData> data = new ArrayList<>();
assertThatEventually(
10000,
() -> {
data.addAll(ReportUtils.getEventMetricDataList(getDevice()));
return data.size();
},
is(3)
);
// Check VmCreationRequested atom
assertThat(data.get(0).getAtom().getPushedCase().getNumber()).isEqualTo(
AtomsProto.Atom.VM_CREATION_REQUESTED_FIELD_NUMBER);
AtomsProto.VmCreationRequested atomVmCreationRequested =
data.get(0).getAtom().getVmCreationRequested();
assertThat(atomVmCreationRequested.getHypervisor())
.isEqualTo(AtomsProto.VmCreationRequested.Hypervisor.PKVM);
assertThat(atomVmCreationRequested.getIsProtected()).isFalse();
assertThat(atomVmCreationRequested.getCreationSucceeded()).isTrue();
assertThat(atomVmCreationRequested.getBinderExceptionCode()).isEqualTo(0);
assertThat(atomVmCreationRequested.getVmIdentifier()).isEqualTo("VmRunApp");
assertThat(atomVmCreationRequested.getConfigType())
.isEqualTo(AtomsProto.VmCreationRequested.ConfigType.VIRTUAL_MACHINE_APP_CONFIG);
assertThat(atomVmCreationRequested.getNumCpus()).isEqualTo(NUM_VCPUS);
assertThat(atomVmCreationRequested.getMemoryMib()).isEqualTo(minMemorySize());
assertThat(atomVmCreationRequested.getApexes())
.isEqualTo("com.android.art:com.android.compos:com.android.sdkext");
// Check VmBooted atom
assertThat(data.get(1).getAtom().getPushedCase().getNumber())
.isEqualTo(AtomsProto.Atom.VM_BOOTED_FIELD_NUMBER);
AtomsProto.VmBooted atomVmBooted = data.get(1).getAtom().getVmBooted();
assertThat(atomVmBooted.getVmIdentifier()).isEqualTo("VmRunApp");
// Check VmExited atom
assertThat(data.get(2).getAtom().getPushedCase().getNumber())
.isEqualTo(AtomsProto.Atom.VM_EXITED_FIELD_NUMBER);
AtomsProto.VmExited atomVmExited = data.get(2).getAtom().getVmExited();
assertThat(atomVmExited.getVmIdentifier()).isEqualTo("VmRunApp");
assertThat(atomVmExited.getDeathReason()).isEqualTo(AtomsProto.VmExited.DeathReason.KILLED);
// Check UID and elapsed_time by comparing each other.
assertThat(atomVmBooted.getUid()).isEqualTo(atomVmCreationRequested.getUid());
assertThat(atomVmExited.getUid()).isEqualTo(atomVmCreationRequested.getUid());
assertThat(atomVmBooted.getElapsedTimeMillis())
.isLessThan(atomVmExited.getElapsedTimeMillis());
}
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-1-2", "9.17/C/1-3"})
public void testMicrodroidBoots() throws Exception {
CommandRunner android = new CommandRunner(getDevice());
final String configPath = "assets/vm_config.json"; // path inside the APK
mMicrodroidDevice =
MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
.debugLevel("full")
.memoryMib(minMemorySize())
.numCpus(NUM_VCPUS)
.build(getAndroidDevice());
mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
String vmList = android.run("/apex/com.android.virt/bin/vm list");
assertThat(vmList).contains("requesterUid: " + android.run("id -u"));
// Test writing to /data partition
microdroid.run("echo MicrodroidTest > /data/local/tmp/test.txt");
assertThat(microdroid.run("cat /data/local/tmp/test.txt")).isEqualTo("MicrodroidTest");
// Check if the APK & its idsig partitions exist
final String apkPartition = "/dev/block/by-name/microdroid-apk";
assertThat(microdroid.run("ls", apkPartition)).isEqualTo(apkPartition);
final String apkIdsigPartition = "/dev/block/by-name/microdroid-apk-idsig";
assertThat(microdroid.run("ls", apkIdsigPartition)).isEqualTo(apkIdsigPartition);
// Check the vm-instance partition as well
final String vmInstancePartition = "/dev/block/by-name/vm-instance";
assertThat(microdroid.run("ls", vmInstancePartition)).isEqualTo(vmInstancePartition);
// Check if the native library in the APK is has correct filesystem info
final String[] abis = microdroid.run("getprop", "ro.product.cpu.abilist").split(",");
assertThat(abis).hasLength(1);
// Check that no denials have happened so far
assertThat(android.tryRun("egrep", "'avc:[[:space:]]{1,2}denied'", LOG_PATH)).isNull();
assertThat(android.tryRun("egrep", "'avc:[[:space:]]{1,2}denied'", CONSOLE_PATH)).isNull();
assertThat(microdroid.run("cat /proc/cpuinfo | grep processor | wc -l"))
.isEqualTo(Integer.toString(NUM_VCPUS));
// Check that selinux is enabled
assertThat(microdroid.run("getenforce")).isEqualTo("Enforcing");
// TODO(b/176805428): adb is broken for nested VM
if (!isCuttlefish()) {
// Check neverallow rules on microdroid
File policyFile = mMicrodroidDevice.pullFile("/sys/fs/selinux/policy");
File generalPolicyConfFile = findTestFile("microdroid_general_sepolicy.conf");
File sepolicyAnalyzeBin = findTestFile("sepolicy-analyze");
CommandResult result =
RunUtil.getDefault()
.runTimedCmd(
10000,
sepolicyAnalyzeBin.getPath(),
policyFile.getPath(),
"neverallow",
"-w",
"-f",
generalPolicyConfFile.getPath());
assertWithMessage("neverallow check failed: " + result.getStderr().trim())
.about(command_results())
.that(result)
.isSuccess();
}
}
@Test
public void testMicrodroidRamUsage() throws Exception {
final String configPath = "assets/vm_config.json";
mMicrodroidDevice =
MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
.debugLevel("full")
.memoryMib(minMemorySize())
.numCpus(NUM_VCPUS)
.build(getAndroidDevice());
mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
mMicrodroidDevice.enableAdbRoot();
CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
Function<String, String> microdroidExec =
(cmd) -> {
try {
return microdroid.run(cmd);
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
};
for (Map.Entry<String, Long> stat :
ProcessUtil.getProcessMemoryMap(microdroidExec).entrySet()) {
mMetrics.addTestMetric(
mMetricPrefix + "meminfo/" + stat.getKey().toLowerCase(),
stat.getValue().toString());
}
for (Map.Entry<Integer, String> proc :
ProcessUtil.getProcessMap(microdroidExec).entrySet()) {
for (Map.Entry<String, Long> stat :
ProcessUtil.getProcessSmapsRollup(proc.getKey(), microdroidExec)
.entrySet()) {
String name = stat.getKey().toLowerCase();
mMetrics.addTestMetric(
mMetricPrefix + "smaps/" + name + "/" + proc.getValue(),
stat.getValue().toString());
}
}
}
@Test
public void testCustomVirtualMachinePermission() throws Exception {
assumeTrue(
"Protected VMs are not supported",
getAndroidDevice().supportsMicrodroid(/*protectedVm=*/ true));
CommandRunner android = new CommandRunner(getDevice());
// Pull etc/microdroid.json
File virtApexDir = FileUtil.createTempDir("virt_apex");
File microdroidConfigFile = new File(virtApexDir, "microdroid.json");
assertThat(getDevice().pullFile(VIRT_APEX + "etc/microdroid.json", microdroidConfigFile))
.isTrue();
JSONObject config = new JSONObject(FileUtil.readStringFromFile(microdroidConfigFile));
// USE_CUSTOM_VIRTUAL_MACHINE is enforced only on protected mode
config.put("protected", true);
// Write updated config
final String configPath = TEST_ROOT + "raw_config.json";
getDevice().pushString(config.toString(), configPath);
// temporarily revoke the permission
android.run(
"pm",
"revoke",
SHELL_PACKAGE_NAME,
"android.permission.USE_CUSTOM_VIRTUAL_MACHINE");
final String ret =
android.runForResult(VIRT_APEX + "bin/vm run", configPath).getStderr().trim();
assertThat(ret)
.contains(
"does not have the android.permission.USE_CUSTOM_VIRTUAL_MACHINE"
+ " permission");
}
@Test
public void testPathToBinaryIsRejected() throws Exception {
CommandRunner android = new CommandRunner(getDevice());
// 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));
final String ret =
android.runForResult(
VIRT_APEX + "bin/vm",
"run-app",
"--payload-binary-name",
"./MicrodroidTestNativeLib.so",
apkPath,
idSigPath,
instanceImgPath)
.getStderr()
.trim();
assertThat(ret).contains("Payload binary name must not specify a path");
}
@Test
@CddTest(requirements = {"9.17/C-2-2", "9.17/C-2-6"})
public void testAllVbmetaUseSHA256() throws Exception {
File virtApexDir = FileUtil.createTempDir("virt_apex");
// Pull the virt apex's etc/ directory (which contains images)
File virtApexEtcDir = new File(virtApexDir, "etc");
// We need only etc/ directory for images
assertWithMessage("Failed to mkdir " + virtApexEtcDir)
.that(virtApexEtcDir.mkdirs())
.isTrue();
assertWithMessage("Failed to pull " + VIRT_APEX + "etc")
.that(getDevice().pullDir(VIRT_APEX + "etc", virtApexEtcDir))
.isTrue();
checkHashAlgorithm(virtApexEtcDir);
}
private String avbInfo(String image_path) throws Exception {
File avbtool = findTestFile("avbtool");
List<String> command =
Arrays.asList(avbtool.getAbsolutePath(), "info_image", "--image", image_path);
CommandResult result =
new RunUtil().runTimedCmd(5000, "/bin/bash", "-c", String.join(" ", command));
String out = result.getStdout();
String err = result.getStderr();
assertWithMessage(
"Command "
+ command
+ " failed."
+ ":\n\tout: "
+ out
+ "\n\terr: "
+ err
+ "\n")
.about(command_results())
.that(result)
.isSuccess();
return out;
}
private void checkHashAlgorithm(File virtApexEtcDir) throws Exception {
List<String> images =
Arrays.asList(
// kernel image (contains descriptors from initrd(s) as well)
"/fs/microdroid_kernel",
// vbmeta partition (contains descriptors from vendor/system images)
"/fs/microdroid_vbmeta.img");
for (String path : images) {
String info = avbInfo(virtApexEtcDir + path);
Pattern pattern = Pattern.compile("Hash Algorithm:[ ]*(sha1|sha256)");
Matcher m = pattern.matcher(info);
while (m.find()) {
assertThat(m.group(1)).isEqualTo("sha256");
}
}
}
@Before
public void setUp() throws Exception {
testIfDeviceIsCapable(getDevice());
mMetricPrefix = getMetricPrefix() + "microdroid/";
mMicrodroidDevice = null;
prepareVirtualizationTestSetup(getDevice());
getDevice().installPackage(findTestFile(APK_NAME), /* reinstall */ false);
// clear the log
getDevice().executeShellV2Command("logcat -c");
}
@After
public void shutdown() throws Exception {
if (mMicrodroidDevice != null) {
getAndroidDevice().shutdownMicrodroid(mMicrodroidDevice);
}
cleanUpVirtualizationTestSetup(getDevice());
archiveLogThenDelete(
mTestLogs, getDevice(), LOG_PATH, "vm.log-" + mTestName.getMethodName());
getDevice().uninstallPackage(PACKAGE_NAME);
// testCustomVirtualMachinePermission revokes this permission. Grant it again as cleanup
new CommandRunner(getDevice())
.tryRun(
"pm",
"grant",
SHELL_PACKAGE_NAME,
"android.permission.USE_CUSTOM_VIRTUAL_MACHINE");
}
private TestDevice getAndroidDevice() {
TestDevice androidDevice = (TestDevice) getDevice();
assertThat(androidDevice).isNotNull();
return androidDevice;
}
}