blob: c6291e45a563095980477b314f24adce47616a14 [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 android.system.virtualmachine.VirtualMachine.STATUS_DELETED;
import static android.system.virtualmachine.VirtualMachine.STATUS_RUNNING;
import static android.system.virtualmachine.VirtualMachine.STATUS_STOPPED;
import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_NON_PROTECTED_VM;
import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_PROTECTED_VM;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Build;
import android.os.IBinder;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.os.SystemProperties;
import android.system.OsConstants;
import android.system.virtualmachine.VirtualMachine;
import android.system.virtualmachine.VirtualMachineCallback;
import android.system.virtualmachine.VirtualMachineConfig;
import android.system.virtualmachine.VirtualMachineDescriptor;
import android.system.virtualmachine.VirtualMachineException;
import android.system.virtualmachine.VirtualMachineManager;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compatibility.common.util.CddTest;
import com.android.compatibility.common.util.VsrTest;
import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
import com.android.microdroid.test.vmshare.IVmShareTestService;
import com.android.microdroid.testservice.IAppCallback;
import com.android.microdroid.testservice.ITestService;
import com.android.microdroid.testservice.IVmCallback;
import com.google.common.base.Strings;
import com.google.common.truth.BooleanSubject;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.OptionalLong;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.MajorType;
@RunWith(Parameterized.class)
public class MicrodroidTests extends MicrodroidDeviceTestBase {
private static final String TAG = "MicrodroidTests";
@Rule public Timeout globalTimeout = Timeout.seconds(300);
private static final String KERNEL_VERSION = SystemProperties.get("ro.kernel.version");
@Parameterized.Parameters(name = "protectedVm={0}")
public static Object[] protectedVmConfigs() {
return new Object[] { false, true };
}
@Parameterized.Parameter public boolean mProtectedVm;
@Before
public void setup() {
grantPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
prepareTestSetup(mProtectedVm);
// USE_CUSTOM_VIRTUAL_MACHINE permission has protection level signature|development, meaning
// that it will be automatically granted when test apk is installed. We have some tests
// checking the behavior when caller doesn't have this permission (e.g.
// createVmWithConfigRequiresPermission). Proactively revoke the permission so that such
// tests can pass when ran by itself, e.g.:
// atest com.android.microdroid.test.MicrodroidTests#createVmWithConfigRequiresPermission
revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
}
@After
public void tearDown() {
revokePermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
}
private static final long ONE_MEBI = 1024 * 1024;
private static final long MIN_MEM_ARM64 = 150 * ONE_MEBI;
private static final long MIN_MEM_X86_64 = 196 * ONE_MEBI;
private static final String EXAMPLE_STRING = "Literally any string!! :)";
private static final String VM_SHARE_APP_PACKAGE_NAME = "com.android.microdroid.vmshare_app";
private void createAndConnectToVmHelper(int cpuTopology) throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
.setCpuTopology(cpuTopology)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mAddInteger = ts.addInteger(123, 456);
tr.mAppRunProp = ts.readProperty("debug.microdroid.app.run");
tr.mSublibRunProp = ts.readProperty("debug.microdroid.app.sublib.run");
tr.mApkContentsPath = ts.getApkContentsPath();
tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
});
testResults.assertNoException();
assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
assertThat(testResults.mAppRunProp).isEqualTo("true");
assertThat(testResults.mSublibRunProp).isEqualTo("true");
assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
}
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
public void createAndConnectToVm() throws Exception {
createAndConnectToVmHelper(CPU_TOPOLOGY_ONE_CPU);
}
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
public void createAndConnectToVm_HostCpuTopology() throws Exception {
createAndConnectToVmHelper(CPU_TOPOLOGY_MATCH_HOST);
}
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
public void createAndRunNoDebugVm() throws Exception {
assumeSupportedDevice();
// For most of our tests we use a debug VM so failures can be diagnosed.
// But we do need non-debug VMs to work, so run one.
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_NONE)
.setVmOutputCaptured(false)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
TestResults testResults =
runVmTestService(TAG, vm, (ts, tr) -> tr.mAddInteger = ts.addInteger(37, 73));
testResults.assertNoException();
assertThat(testResults.mAddInteger).isEqualTo(37 + 73);
}
@Test
@CddTest(
requirements = {
"9.17/C-1-1",
"9.17/C-1-2",
"9.17/C-1-4",
})
public void createVmRequiresPermission() {
assumeSupportedDevice();
revokePermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.build();
SecurityException e =
assertThrows(
SecurityException.class,
() -> forceCreateNewVirtualMachine("test_vm_requires_permission", config));
assertThat(e).hasMessageThat()
.contains("android.permission.MANAGE_VIRTUAL_MACHINE permission");
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void autoCloseVm() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
try (VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config)) {
assertThat(vm.getStatus()).isEqualTo(STATUS_STOPPED);
// close() implicitly called on stopped VM.
}
try (VirtualMachine vm = getVirtualMachineManager().get("test_vm")) {
vm.run();
assertThat(vm.getStatus()).isEqualTo(STATUS_RUNNING);
// close() implicitly called on running VM.
}
try (VirtualMachine vm = getVirtualMachineManager().get("test_vm")) {
assertThat(vm.getStatus()).isEqualTo(STATUS_STOPPED);
getVirtualMachineManager().delete("test_vm");
assertThat(vm.getStatus()).isEqualTo(STATUS_DELETED);
// close() implicitly called on deleted VM.
}
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void autoCloseVmDescriptor() throws Exception {
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
VirtualMachineDescriptor descriptor = vm.toDescriptor();
Parcel parcel = Parcel.obtain();
try (descriptor) {
// It should be ok to use at this point
descriptor.writeToParcel(parcel, 0);
}
// But not now - it's been closed.
assertThrows(IllegalStateException.class, () -> descriptor.writeToParcel(parcel, 0));
assertThrows(
IllegalStateException.class,
() -> getVirtualMachineManager().importFromDescriptor("imported_vm", descriptor));
// Closing again is fine.
descriptor.close();
// Tidy up
parcel.recycle();
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void vmDescriptorClosedOnImport() throws Exception {
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
VirtualMachineDescriptor descriptor = vm.toDescriptor();
getVirtualMachineManager().importFromDescriptor("imported_vm", descriptor);
try {
// Descriptor has been implicitly closed
assertThrows(
IllegalStateException.class,
() ->
getVirtualMachineManager()
.importFromDescriptor("imported_vm2", descriptor));
} finally {
getVirtualMachineManager().delete("imported_vm");
}
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void vmLifecycleChecks() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
assertThat(vm.getStatus()).isEqualTo(STATUS_STOPPED);
// These methods require a running VM
assertThrowsVmExceptionContaining(
() -> vm.connectVsock(VirtualMachine.MIN_VSOCK_PORT), "not in running state");
assertThrowsVmExceptionContaining(
() -> vm.connectToVsockServer(VirtualMachine.MIN_VSOCK_PORT),
"not in running state");
vm.run();
assertThat(vm.getStatus()).isEqualTo(STATUS_RUNNING);
// These methods require a stopped VM
assertThrowsVmExceptionContaining(() -> vm.run(), "not in stopped state");
assertThrowsVmExceptionContaining(() -> vm.setConfig(config), "not in stopped state");
assertThrowsVmExceptionContaining(() -> vm.toDescriptor(), "not in stopped state");
assertThrowsVmExceptionContaining(
() -> getVirtualMachineManager().delete("test_vm"), "not in stopped state");
vm.stop();
getVirtualMachineManager().delete("test_vm");
assertThat(vm.getStatus()).isEqualTo(STATUS_DELETED);
// None of these should work for a deleted VM
assertThrowsVmExceptionContaining(
() -> vm.connectVsock(VirtualMachine.MIN_VSOCK_PORT), "deleted");
assertThrowsVmExceptionContaining(
() -> vm.connectToVsockServer(VirtualMachine.MIN_VSOCK_PORT), "deleted");
assertThrowsVmExceptionContaining(() -> vm.run(), "deleted");
assertThrowsVmExceptionContaining(() -> vm.setConfig(config), "deleted");
assertThrowsVmExceptionContaining(() -> vm.toDescriptor(), "deleted");
// This is indistinguishable from the VM having never existed, so the message
// is non-specific.
assertThrowsVmException(() -> getVirtualMachineManager().delete("test_vm"));
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void connectVsock() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_vsock", config);
AtomicReference<String> response = new AtomicReference<>();
String request = "Look not into the abyss";
TestResults testResults =
runVmTestService(
TAG,
vm,
(service, results) -> {
service.runEchoReverseServer();
ParcelFileDescriptor pfd =
vm.connectVsock(ITestService.ECHO_REVERSE_PORT);
try (InputStream input = new AutoCloseInputStream(pfd);
OutputStream output = new AutoCloseOutputStream(pfd)) {
BufferedReader reader =
new BufferedReader(new InputStreamReader(input));
Writer writer = new OutputStreamWriter(output);
writer.write(request + "\n");
writer.flush();
response.set(reader.readLine());
}
});
testResults.assertNoException();
assertThat(response.get()).isEqualTo(new StringBuilder(request).reverse().toString());
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void binderCallbacksWork() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
String request = "Hello";
CompletableFuture<String> response = new CompletableFuture<>();
IAppCallback appCallback =
new IAppCallback.Stub() {
@Override
public void setVmCallback(IVmCallback vmCallback) {
// Do this on a separate thread to simulate an asynchronous trigger,
// and to make sure it doesn't happen in the context of an inbound binder
// call.
new Thread() {
@Override
public void run() {
try {
vmCallback.echoMessage(request);
} catch (Exception e) {
response.completeExceptionally(e);
}
}
}.start();
}
@Override
public void onEchoRequestReceived(String message) {
response.complete(message);
}
};
TestResults testResults =
runVmTestService(
TAG,
vm,
(service, results) -> {
service.requestCallback(appCallback);
response.get(10, TimeUnit.SECONDS);
});
testResults.assertNoException();
assertThat(response.getNow("no response")).isEqualTo("Received: " + request);
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void vmConfigGetAndSetTests() {
// Minimal has as little as specified as possible; everything that can be is defaulted.
VirtualMachineConfig.Builder minimalBuilder = newVmConfigBuilder();
VirtualMachineConfig minimal = minimalBuilder.setPayloadBinaryName("binary.so").build();
assertThat(minimal.getApkPath()).isNull();
assertThat(minimal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_NONE);
assertThat(minimal.getMemoryBytes()).isEqualTo(0);
assertThat(minimal.getCpuTopology()).isEqualTo(CPU_TOPOLOGY_ONE_CPU);
assertThat(minimal.getPayloadBinaryName()).isEqualTo("binary.so");
assertThat(minimal.getPayloadConfigPath()).isNull();
assertThat(minimal.isProtectedVm()).isEqualTo(isProtectedVm());
assertThat(minimal.isEncryptedStorageEnabled()).isFalse();
assertThat(minimal.getEncryptedStorageBytes()).isEqualTo(0);
assertThat(minimal.isVmOutputCaptured()).isEqualTo(false);
// Maximal has everything that can be set to some non-default value. (And has different
// values than minimal for the required fields.)
VirtualMachineConfig.Builder maximalBuilder =
new VirtualMachineConfig.Builder(getContext())
.setProtectedVm(mProtectedVm)
.setPayloadConfigPath("config/path")
.setApkPath("/apk/path")
.setDebugLevel(DEBUG_LEVEL_FULL)
.setMemoryBytes(42)
.setCpuTopology(CPU_TOPOLOGY_MATCH_HOST)
.setEncryptedStorageBytes(1_000_000)
.setVmOutputCaptured(true);
VirtualMachineConfig maximal = maximalBuilder.build();
assertThat(maximal.getApkPath()).isEqualTo("/apk/path");
assertThat(maximal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_FULL);
assertThat(maximal.getMemoryBytes()).isEqualTo(42);
assertThat(maximal.getCpuTopology()).isEqualTo(CPU_TOPOLOGY_MATCH_HOST);
assertThat(maximal.getPayloadBinaryName()).isNull();
assertThat(maximal.getPayloadConfigPath()).isEqualTo("config/path");
assertThat(maximal.isProtectedVm()).isEqualTo(isProtectedVm());
assertThat(maximal.isEncryptedStorageEnabled()).isTrue();
assertThat(maximal.getEncryptedStorageBytes()).isEqualTo(1_000_000);
assertThat(maximal.isVmOutputCaptured()).isEqualTo(true);
assertThat(minimal.isCompatibleWith(maximal)).isFalse();
assertThat(minimal.isCompatibleWith(minimal)).isTrue();
assertThat(maximal.isCompatibleWith(maximal)).isTrue();
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void vmConfigBuilderValidationTests() {
VirtualMachineConfig.Builder builder = newVmConfigBuilder();
// All your null are belong to me.
assertThrows(NullPointerException.class, () -> new VirtualMachineConfig.Builder(null));
assertThrows(NullPointerException.class, () -> builder.setApkPath(null));
assertThrows(NullPointerException.class, () -> builder.setPayloadConfigPath(null));
assertThrows(NullPointerException.class, () -> builder.setPayloadBinaryName(null));
assertThrows(NullPointerException.class, () -> builder.setPayloadConfigPath(null));
// Individual property checks.
assertThrows(
IllegalArgumentException.class, () -> builder.setApkPath("relative/path/to.apk"));
assertThrows(
IllegalArgumentException.class, () -> builder.setPayloadBinaryName("dir/file.so"));
assertThrows(IllegalArgumentException.class, () -> builder.setDebugLevel(-1));
assertThrows(IllegalArgumentException.class, () -> builder.setMemoryBytes(0));
assertThrows(IllegalArgumentException.class, () -> builder.setCpuTopology(-1));
assertThrows(IllegalArgumentException.class, () -> builder.setEncryptedStorageBytes(0));
// Consistency checks enforced at build time.
Exception e;
e = assertThrows(IllegalStateException.class, () -> builder.build());
assertThat(e).hasMessageThat().contains("setPayloadBinaryName must be called");
VirtualMachineConfig.Builder protectedNotSet =
new VirtualMachineConfig.Builder(getContext()).setPayloadBinaryName("binary.so");
e = assertThrows(IllegalStateException.class, () -> protectedNotSet.build());
assertThat(e).hasMessageThat().contains("setProtectedVm must be called");
VirtualMachineConfig.Builder captureOutputOnNonDebuggable =
newVmConfigBuilder()
.setPayloadBinaryName("binary.so")
.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_NONE)
.setVmOutputCaptured(true);
e = assertThrows(IllegalStateException.class, () -> captureOutputOnNonDebuggable.build());
assertThat(e).hasMessageThat().contains("debug level must be FULL to capture output");
VirtualMachineConfig.Builder captureInputOnNonDebuggable =
newVmConfigBuilder()
.setPayloadBinaryName("binary.so")
.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_NONE)
.setVmConsoleInputSupported(true);
e = assertThrows(IllegalStateException.class, () -> captureInputOnNonDebuggable.build());
assertThat(e).hasMessageThat().contains("debug level must be FULL to use console input");
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void compatibleConfigTests() {
VirtualMachineConfig baseline = newBaselineBuilder().build();
// A config must be compatible with itself
assertConfigCompatible(baseline, newBaselineBuilder()).isTrue();
// Changes that must always be compatible
assertConfigCompatible(baseline, newBaselineBuilder().setMemoryBytes(99)).isTrue();
assertConfigCompatible(
baseline, newBaselineBuilder().setCpuTopology(CPU_TOPOLOGY_MATCH_HOST))
.isTrue();
// Changes that must be incompatible, since they must change the VM identity.
assertConfigCompatible(baseline, newBaselineBuilder().setDebugLevel(DEBUG_LEVEL_FULL))
.isFalse();
assertConfigCompatible(baseline, newBaselineBuilder().setPayloadBinaryName("different"))
.isFalse();
int capabilities = getVirtualMachineManager().getCapabilities();
if ((capabilities & CAPABILITY_PROTECTED_VM) != 0
&& (capabilities & CAPABILITY_NON_PROTECTED_VM) != 0) {
assertConfigCompatible(baseline, newBaselineBuilder().setProtectedVm(!isProtectedVm()))
.isFalse();
}
// Changes that are currently incompatible for ease of implementation, but this might change
// in the future.
assertConfigCompatible(baseline, newBaselineBuilder().setApkPath("/different")).isFalse();
assertConfigCompatible(baseline, newBaselineBuilder().setEncryptedStorageBytes(100_000))
.isFalse();
VirtualMachineConfig.Builder debuggableBuilder =
newBaselineBuilder().setDebugLevel(DEBUG_LEVEL_FULL);
VirtualMachineConfig debuggable = debuggableBuilder.build();
assertConfigCompatible(debuggable, debuggableBuilder.setVmOutputCaptured(true)).isFalse();
assertConfigCompatible(debuggable, debuggableBuilder.setVmOutputCaptured(false)).isTrue();
assertConfigCompatible(debuggable, debuggableBuilder.setVmConsoleInputSupported(true))
.isFalse();
VirtualMachineConfig currentContextConfig =
new VirtualMachineConfig.Builder(getContext())
.setProtectedVm(isProtectedVm())
.setPayloadBinaryName("binary.so")
.build();
// packageName is not directly exposed by the config, so we have to be a bit creative
// to modify it.
Context otherContext =
new ContextWrapper(getContext()) {
@Override
public String getPackageName() {
return "other.package.name";
}
};
VirtualMachineConfig.Builder otherContextBuilder =
new VirtualMachineConfig.Builder(otherContext)
.setProtectedVm(isProtectedVm())
.setPayloadBinaryName("binary.so");
assertConfigCompatible(currentContextConfig, otherContextBuilder).isFalse();
}
private VirtualMachineConfig.Builder newBaselineBuilder() {
return newVmConfigBuilder().setPayloadBinaryName("binary.so").setApkPath("/apk/path");
}
private BooleanSubject assertConfigCompatible(
VirtualMachineConfig baseline, VirtualMachineConfig.Builder builder) {
return assertThat(builder.build().isCompatibleWith(baseline));
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void vmUnitTests() throws Exception {
VirtualMachineConfig.Builder builder =
newVmConfigBuilder().setPayloadBinaryName("binary.so");
VirtualMachineConfig config = builder.build();
VirtualMachine vm = forceCreateNewVirtualMachine("vm_name", config);
assertThat(vm.getName()).isEqualTo("vm_name");
assertThat(vm.getConfig().getPayloadBinaryName()).isEqualTo("binary.so");
assertThat(vm.getConfig().getMemoryBytes()).isEqualTo(0);
VirtualMachineConfig compatibleConfig = builder.setMemoryBytes(42).build();
vm.setConfig(compatibleConfig);
assertThat(vm.getName()).isEqualTo("vm_name");
assertThat(vm.getConfig().getPayloadBinaryName()).isEqualTo("binary.so");
assertThat(vm.getConfig().getMemoryBytes()).isEqualTo(42);
assertThat(getVirtualMachineManager().get("vm_name")).isSameInstanceAs(vm);
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void testAvfRequiresUpdatableApex() throws Exception {
assertWithMessage("Devices that support AVF must also support updatable APEX")
.that(SystemProperties.getBoolean("ro.apex.updatable", false))
.isTrue();
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void vmmGetAndCreate() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachineManager vmm = getVirtualMachineManager();
String vmName = "vmName";
try {
// VM does not yet exist
assertThat(vmm.get(vmName)).isNull();
VirtualMachine vm1 = vmm.create(vmName, config);
// Now it does, and we should get the same instance back
assertThat(vmm.get(vmName)).isSameInstanceAs(vm1);
assertThat(vmm.getOrCreate(vmName, config)).isSameInstanceAs(vm1);
// Can't recreate it though
assertThrowsVmException(() -> vmm.create(vmName, config));
vmm.delete(vmName);
assertThat(vmm.get(vmName)).isNull();
// Now that we deleted the old one, this should create rather than get, and it should be
// a new instance.
VirtualMachine vm2 = vmm.getOrCreate(vmName, config);
assertThat(vm2).isNotSameInstanceAs(vm1);
// The old one must remain deleted, or we'd have two VirtualMachine instances referring
// to the same VM.
assertThat(vm1.getStatus()).isEqualTo(STATUS_DELETED);
// Subsequent gets should return this new one.
assertThat(vmm.get(vmName)).isSameInstanceAs(vm2);
assertThat(vmm.getOrCreate(vmName, config)).isSameInstanceAs(vm2);
} finally {
vmm.delete(vmName);
}
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void vmFilesStoredInDeDirWhenCreatedFromDEContext() throws Exception {
final Context ctx = getContext().createDeviceProtectedStorageContext();
final int userId = ctx.getUserId();
final VirtualMachineManager vmm = ctx.getSystemService(VirtualMachineManager.class);
VirtualMachineConfig config =
newVmConfigBuilder().setPayloadBinaryName("binary.so").build();
try {
VirtualMachine vm = vmm.create("vm-name", config);
// TODO(b/261430346): what about non-primary user?
assertThat(vm.getRootDir().getAbsolutePath())
.isEqualTo(
"/data/user_de/" + userId + "/com.android.microdroid.test/vm/vm-name");
} finally {
vmm.delete("vm-name");
}
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void vmFilesStoredInCeDirWhenCreatedFromCEContext() throws Exception {
final Context ctx = getContext().createCredentialProtectedStorageContext();
final int userId = ctx.getUserId();
final VirtualMachineManager vmm = ctx.getSystemService(VirtualMachineManager.class);
VirtualMachineConfig config =
newVmConfigBuilder().setPayloadBinaryName("binary.so").build();
try {
VirtualMachine vm = vmm.create("vm-name", config);
// TODO(b/261430346): what about non-primary user?
assertThat(vm.getRootDir().getAbsolutePath())
.isEqualTo("/data/user/" + userId + "/com.android.microdroid.test/vm/vm-name");
} finally {
vmm.delete("vm-name");
}
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void differentManagersForDifferentContexts() throws Exception {
final Context ceCtx = getContext().createCredentialProtectedStorageContext();
final Context deCtx = getContext().createDeviceProtectedStorageContext();
assertThat(ceCtx.getSystemService(VirtualMachineManager.class))
.isNotSameInstanceAs(deCtx.getSystemService(VirtualMachineManager.class));
}
@Test
@CddTest(requirements = {
"9.17/C-1-1",
"9.17/C-1-2",
"9.17/C-1-4",
})
public void createVmWithConfigRequiresPermission() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config.json")
.setMemoryBytes(minMemoryRequired())
.build();
VirtualMachine vm =
forceCreateNewVirtualMachine("test_vm_config_requires_permission", config);
SecurityException e =
assertThrows(
SecurityException.class, () -> runVmTestService(TAG, vm, (ts, tr) -> {}));
assertThat(e).hasMessageThat()
.contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission");
}
@Test
@CddTest(requirements = {
"9.17/C-1-1",
})
public void deleteVm() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_delete", config);
VirtualMachineManager vmm = getVirtualMachineManager();
vmm.delete("test_vm_delete");
// VM should no longer exist
assertThat(vmm.get("test_vm_delete")).isNull();
// Can't start the VM even with an existing reference
assertThrowsVmException(vm::run);
// Can't delete the VM since it no longer exists
assertThrowsVmException(() -> vmm.delete("test_vm_delete"));
}
@Test
@CddTest(
requirements = {
"9.17/C-1-1",
})
public void deleteVmFiles() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidExitNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_delete", config);
vm.run();
// If we explicitly stop a VM, that triggers some tidy up; so for this test we start a VM
// that immediately stops itself.
while (vm.getStatus() == STATUS_RUNNING) {
Thread.sleep(100);
}
// Delete the files without telling VMM. This isn't a good idea, but we can't stop an
// app doing it, and we should recover from it.
for (File f : vm.getRootDir().listFiles()) {
Files.delete(f.toPath());
}
vm.getRootDir().delete();
VirtualMachineManager vmm = getVirtualMachineManager();
assertThat(vmm.get("test_vm_delete")).isNull();
assertThat(vm.getStatus()).isEqualTo(STATUS_DELETED);
}
@Test
@CddTest(requirements = {
"9.17/C-1-1",
})
public void validApkPathIsAccepted() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setApkPath(getContext().getPackageCodePath())
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_explicit_apk_path", config);
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mApkContentsPath = ts.getApkContentsPath();
});
testResults.assertNoException();
assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void invalidVmNameIsRejected() {
VirtualMachineManager vmm = getVirtualMachineManager();
assertThrows(IllegalArgumentException.class, () -> vmm.get("../foo"));
assertThrows(IllegalArgumentException.class, () -> vmm.get(".."));
}
@Test
@CddTest(requirements = {
"9.17/C-1-1",
"9.17/C-2-1"
})
public void extraApk() throws Exception {
assumeSupportedDevice();
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config_extra_apk.json")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_extra_apk", config);
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mExtraApkTestProp =
ts.readProperty("debug.microdroid.test.extra_apk");
});
assertThat(testResults.mExtraApkTestProp).isEqualTo("PASS");
}
@Test
public void bootFailsWhenLowMem() throws Exception {
for (int memMib : new int[]{ 10, 20, 40 }) {
VirtualMachineConfig lowMemConfig =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(memMib)
.setDebugLevel(DEBUG_LEVEL_NONE)
.setVmOutputCaptured(false)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("low_mem", lowMemConfig);
final CompletableFuture<Boolean> onPayloadReadyExecuted = new CompletableFuture<>();
final CompletableFuture<Boolean> onStoppedExecuted = new CompletableFuture<>();
VmEventListener listener =
new VmEventListener() {
@Override
public void onPayloadReady(VirtualMachine vm) {
onPayloadReadyExecuted.complete(true);
super.onPayloadReady(vm);
}
@Override
public void onStopped(VirtualMachine vm, int reason) {
onStoppedExecuted.complete(true);
super.onStopped(vm, reason);
}
};
listener.runToFinish(TAG, vm);
// Assert that onStopped() was executed but onPayloadReady() was never run
assertThat(onStoppedExecuted.getNow(false)).isTrue();
assertThat(onPayloadReadyExecuted.getNow(false)).isFalse();
}
}
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
public void changingNonDebuggableVmDebuggableInvalidatesVmIdentity() throws Exception {
// Debuggability changes initrd which is verified by pvmfw.
// Therefore, skip this on non-protected VM.
assumeProtectedVM();
changeDebugLevel(DEBUG_LEVEL_NONE, DEBUG_LEVEL_FULL);
}
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
public void changingDebuggableVmNonDebuggableInvalidatesVmIdentity() throws Exception {
// Debuggability changes initrd which is verified by pvmfw.
// Therefore, skip this on non-protected VM.
assumeProtectedVM();
changeDebugLevel(DEBUG_LEVEL_FULL, DEBUG_LEVEL_NONE);
}
private void changeDebugLevel(int fromLevel, int toLevel) throws Exception {
assumeSupportedDevice();
VirtualMachineConfig.Builder builder =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(fromLevel)
.setVmOutputCaptured(false);
VirtualMachineConfig normalConfig = builder.build();
forceCreateNewVirtualMachine("test_vm", normalConfig);
assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isTrue();
// Try to run the VM again with the previous instance.img
// We need to make sure that no changes on config don't invalidate the identity, to compare
// the result with the below "different debug level" test.
File vmInstance = getVmFile("test_vm", "instance.img");
File vmInstanceBackup = File.createTempFile("instance", ".img");
Files.copy(vmInstance.toPath(), vmInstanceBackup.toPath(), REPLACE_EXISTING);
forceCreateNewVirtualMachine("test_vm", normalConfig);
Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isTrue();
// Launch the same VM with a different debug level. The Java API prohibits this
// (thankfully).
// For testing, we do that by creating a new VM with debug level, and copy the old instance
// image to the new VM instance image.
VirtualMachineConfig debugConfig = builder.setDebugLevel(toLevel).build();
forceCreateNewVirtualMachine("test_vm", debugConfig);
Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING);
assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isFalse();
}
private static class VmCdis {
public byte[] cdiAttest;
public byte[] instanceSecret;
}
private VmCdis launchVmAndGetCdis(String instanceName) throws Exception {
VirtualMachine vm = getVirtualMachineManager().get(instanceName);
VmCdis vmCdis = new VmCdis();
CompletableFuture<Exception> exception = new CompletableFuture<>();
VmEventListener listener =
new VmEventListener() {
@Override
public void onPayloadReady(VirtualMachine vm) {
try {
ITestService testService =
ITestService.Stub.asInterface(
vm.connectToVsockServer(ITestService.PORT));
vmCdis.cdiAttest = testService.insecurelyExposeAttestationCdi();
vmCdis.instanceSecret = testService.insecurelyExposeVmInstanceSecret();
} catch (Exception e) {
exception.complete(e);
} finally {
forceStop(vm);
}
}
};
listener.runToFinish(TAG, vm);
Exception e = exception.getNow(null);
if (e != null) {
throw new RuntimeException(e);
}
return vmCdis;
}
@Test
@CddTest(requirements = {
"9.17/C-1-1",
"9.17/C-2-7"
})
public void instancesOfSameVmHaveDifferentCdis() throws Exception {
assumeSupportedDevice();
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig normalConfig =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config.json")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
forceCreateNewVirtualMachine("test_vm_a", normalConfig);
forceCreateNewVirtualMachine("test_vm_b", normalConfig);
VmCdis vm_a_cdis = launchVmAndGetCdis("test_vm_a");
VmCdis vm_b_cdis = launchVmAndGetCdis("test_vm_b");
assertThat(vm_a_cdis.cdiAttest).isNotNull();
assertThat(vm_b_cdis.cdiAttest).isNotNull();
assertThat(vm_a_cdis.cdiAttest).isNotEqualTo(vm_b_cdis.cdiAttest);
assertThat(vm_a_cdis.instanceSecret).isNotNull();
assertThat(vm_b_cdis.instanceSecret).isNotNull();
assertThat(vm_a_cdis.instanceSecret).isNotEqualTo(vm_b_cdis.instanceSecret);
}
@Test
@CddTest(requirements = {
"9.17/C-1-1",
"9.17/C-2-7"
})
public void sameInstanceKeepsSameCdis() throws Exception {
assumeSupportedDevice();
assume().withMessage("Skip on CF. Too Slow. b/257270529").that(isCuttlefish()).isFalse();
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig normalConfig =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config.json")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
forceCreateNewVirtualMachine("test_vm", normalConfig);
VmCdis first_boot_cdis = launchVmAndGetCdis("test_vm");
VmCdis second_boot_cdis = launchVmAndGetCdis("test_vm");
// The attestation CDI isn't specified to be stable, though it might be
assertThat(first_boot_cdis.instanceSecret).isNotNull();
assertThat(second_boot_cdis.instanceSecret).isNotNull();
assertThat(first_boot_cdis.instanceSecret).isEqualTo(second_boot_cdis.instanceSecret);
}
@Test
@CddTest(requirements = {
"9.17/C-1-1",
"9.17/C-2-7"
})
public void bccIsSuperficiallyWellFormed() throws Exception {
assumeSupportedDevice();
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig normalConfig =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config.json")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("bcc_vm", normalConfig);
TestResults testResults =
runVmTestService(
TAG,
vm,
(service, results) -> {
results.mBcc = service.getBcc();
});
testResults.assertNoException();
byte[] bccBytes = testResults.mBcc;
assertThat(bccBytes).isNotNull();
ByteArrayInputStream bais = new ByteArrayInputStream(bccBytes);
List<DataItem> dataItems = new CborDecoder(bais).decode();
assertThat(dataItems.size()).isEqualTo(1);
assertThat(dataItems.get(0).getMajorType()).isEqualTo(MajorType.ARRAY);
List<DataItem> rootArrayItems = ((Array) dataItems.get(0)).getDataItems();
assertThat(rootArrayItems.size()).isAtLeast(2); // Root public key and one certificate
if (mProtectedVm) {
if (isFeatureEnabled(VirtualMachineManager.FEATURE_DICE_CHANGES)) {
// When a true DICE chain is created, we expect the root public key, at least one
// entry for the boot before pvmfw, then pvmfw, vm_entry (Microdroid kernel) and
// Microdroid payload entries.
assertThat(rootArrayItems.size()).isAtLeast(5);
} else {
// pvmfw truncates the DICE chain it gets, so we expect exactly entries for
// public key, vm_entry (Microdroid kernel) and Microdroid payload.
assertThat(rootArrayItems.size()).isEqualTo(3);
}
}
}
@Test
@CddTest(requirements = {
"9.17/C-1-1",
"9.17/C-1-2"
})
public void accessToCdisIsRestricted() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
forceCreateNewVirtualMachine("test_vm", config);
assertThrows(Exception.class, () -> launchVmAndGetCdis("test_vm"));
}
@Test
public void isFeatureEnabled_requiresManagePermission() throws Exception {
revokePermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineManager vmm = getVirtualMachineManager();
SecurityException e =
assertThrows(SecurityException.class, () -> vmm.isFeatureEnabled("whatever"));
assertThat(e)
.hasMessageThat()
.contains("android.permission.MANAGE_VIRTUAL_MACHINE permission");
}
private static final UUID MICRODROID_PARTITION_UUID =
UUID.fromString("cf9afe9a-0662-11ec-a329-c32663a09d75");
private static final UUID PVM_FW_PARTITION_UUID =
UUID.fromString("90d2174a-038a-4bc6-adf3-824848fc5825");
private static final long BLOCK_SIZE = 512;
// Find the starting offset which holds the data of a partition having UUID.
// This is a kind of hack; rather than parsing QCOW2 we exploit the fact that the cluster size
// is normally greater than 512. It implies that the partition data should exist at a block
// which follows the header block
private OptionalLong findPartitionDataOffset(RandomAccessFile file, UUID uuid)
throws IOException {
// For each 512-byte block in file, check header
long fileSize = file.length();
for (long idx = 0; idx + BLOCK_SIZE < fileSize; idx += BLOCK_SIZE) {
file.seek(idx);
long high = file.readLong();
long low = file.readLong();
if (uuid.equals(new UUID(high, low))) return OptionalLong.of(idx + BLOCK_SIZE);
}
return OptionalLong.empty();
}
private void flipBit(RandomAccessFile file, long offset) throws IOException {
file.seek(offset);
int b = file.readByte();
file.seek(offset);
file.writeByte(b ^ 1);
}
private RandomAccessFile prepareInstanceImage(String vmName) throws Exception {
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
forceCreateNewVirtualMachine(vmName, config);
assertThat(tryBootVm(TAG, vmName).payloadStarted).isTrue();
File instanceImgPath = getVmFile(vmName, "instance.img");
return new RandomAccessFile(instanceImgPath, "rw");
}
private void assertThatPartitionIsMissing(UUID partitionUuid) throws Exception {
RandomAccessFile instanceFile = prepareInstanceImage("test_vm_integrity");
assertThat(findPartitionDataOffset(instanceFile, partitionUuid).isPresent())
.isFalse();
}
// Flips a bit of given partition, and then see if boot fails.
private void assertThatBootFailsAfterCompromisingPartition(UUID partitionUuid)
throws Exception {
RandomAccessFile instanceFile = prepareInstanceImage("test_vm_integrity");
OptionalLong offset = findPartitionDataOffset(instanceFile, partitionUuid);
assertThat(offset.isPresent()).isTrue();
flipBit(instanceFile, offset.getAsLong());
BootResult result = tryBootVm(TAG, "test_vm_integrity");
assertThat(result.payloadStarted).isFalse();
// This failure should shut the VM down immediately and shouldn't trigger a hangup.
assertThat(result.deathReason).isNotEqualTo(VirtualMachineCallback.STOP_REASON_HANGUP);
}
@Test
@CddTest(requirements = {
"9.17/C-1-1",
"9.17/C-2-7"
})
public void bootFailsWhenMicrodroidDataIsCompromised() throws Exception {
assertThatBootFailsAfterCompromisingPartition(MICRODROID_PARTITION_UUID);
}
@Test
@CddTest(requirements = {
"9.17/C-1-1",
"9.17/C-2-7"
})
public void bootFailsWhenPvmFwDataIsCompromised() throws Exception {
if (mProtectedVm) {
assertThatBootFailsAfterCompromisingPartition(PVM_FW_PARTITION_UUID);
} else {
// non-protected VM shouldn't have pvmfw data
assertThatPartitionIsMissing(PVM_FW_PARTITION_UUID);
}
}
@Test
public void bootFailsWhenConfigIsInvalid() throws Exception {
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig normalConfig =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config_no_task.json")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
forceCreateNewVirtualMachine("test_vm_invalid_config", normalConfig);
BootResult bootResult = tryBootVm(TAG, "test_vm_invalid_config");
assertThat(bootResult.payloadStarted).isFalse();
assertThat(bootResult.deathReason).isEqualTo(
VirtualMachineCallback.STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG);
}
@Test
public void bootFailsWhenBinaryNameIsInvalid() throws Exception {
VirtualMachineConfig.Builder builder =
newVmConfigBuilder().setPayloadBinaryName("DoesNotExist.so");
VirtualMachineConfig normalConfig = builder.setDebugLevel(DEBUG_LEVEL_FULL).build();
forceCreateNewVirtualMachine("test_vm_invalid_binary_path", normalConfig);
BootResult bootResult = tryBootVm(TAG, "test_vm_invalid_binary_path");
assertThat(bootResult.payloadStarted).isFalse();
assertThat(bootResult.deathReason).isEqualTo(
VirtualMachineCallback.STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR);
}
// Checks whether microdroid_launcher started but payload failed. reason must be recorded in the
// console output.
private void assertThatPayloadFailsDueTo(VirtualMachine vm, String reason) throws Exception {
final CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
final CompletableFuture<Integer> exitCodeFuture = new CompletableFuture<>();
VmEventListener listener =
new VmEventListener() {
@Override
public void onPayloadStarted(VirtualMachine vm) {
payloadStarted.complete(true);
}
@Override
public void onPayloadFinished(VirtualMachine vm, int exitCode) {
exitCodeFuture.complete(exitCode);
}
};
listener.runToFinish(TAG, vm);
assertThat(payloadStarted.getNow(false)).isTrue();
assertThat(exitCodeFuture.getNow(0)).isNotEqualTo(0);
assertThat(listener.getLogOutput()).contains(reason);
}
@Test
public void bootFailsWhenBinaryIsMissingEntryFunction() throws Exception {
VirtualMachineConfig normalConfig =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidEmptyNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.setVmOutputCaptured(true)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_missing_entry", normalConfig);
assertThatPayloadFailsDueTo(vm, "Failed to find entrypoint");
}
@Test
public void bootFailsWhenBinaryTriesToLinkAgainstPrivateLibs() throws Exception {
VirtualMachineConfig normalConfig =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidPrivateLinkingNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.setVmOutputCaptured(true)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_private_linking", normalConfig);
assertThatPayloadFailsDueTo(vm, "Failed to dlopen");
}
@Test
public void sameInstancesShareTheSameVmObject() throws Exception {
VirtualMachineConfig config =
newVmConfigBuilder().setPayloadBinaryName("MicrodroidTestNativeLib.so").build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
VirtualMachine vm2 = getVirtualMachineManager().get("test_vm");
assertThat(vm).isEqualTo(vm2);
VirtualMachine newVm = forceCreateNewVirtualMachine("test_vm", config);
VirtualMachine newVm2 = getVirtualMachineManager().get("test_vm");
assertThat(newVm).isEqualTo(newVm2);
assertThat(vm).isNotEqualTo(newVm);
}
@Test
public void importedVmAndOriginalVmHaveTheSameCdi() throws Exception {
assumeSupportedDevice();
// Arrange
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config.json")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
String vmNameOrig = "test_vm_orig";
String vmNameImport = "test_vm_import";
VirtualMachine vmOrig = forceCreateNewVirtualMachine(vmNameOrig, config);
VmCdis origCdis = launchVmAndGetCdis(vmNameOrig);
assertThat(origCdis.instanceSecret).isNotNull();
VirtualMachineManager vmm = getVirtualMachineManager();
if (vmm.get(vmNameImport) != null) {
vmm.delete(vmNameImport);
}
// Action
// The imported VM will be fetched by name later.
vmm.importFromDescriptor(vmNameImport, vmOrig.toDescriptor());
// Asserts
VmCdis importCdis = launchVmAndGetCdis(vmNameImport);
assertThat(origCdis.instanceSecret).isEqualTo(importCdis.instanceSecret);
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void importedVmIsEqualToTheOriginalVm_WithoutStorage() throws Exception {
TestResults testResults = importedVmIsEqualToTheOriginalVm(false);
assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void importedVmIsEqualToTheOriginalVm_WithStorage() throws Exception {
TestResults testResults = importedVmIsEqualToTheOriginalVm(true);
assertThat(testResults.mEncryptedStoragePath).isEqualTo("/mnt/encryptedstore");
}
private TestResults importedVmIsEqualToTheOriginalVm(boolean encryptedStoreEnabled)
throws Exception {
// Arrange
VirtualMachineConfig.Builder builder =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL);
if (encryptedStoreEnabled) {
builder.setEncryptedStorageBytes(4_000_000);
}
VirtualMachineConfig config = builder.build();
String vmNameOrig = "test_vm_orig";
String vmNameImport = "test_vm_import";
VirtualMachine vmOrig = forceCreateNewVirtualMachine(vmNameOrig, config);
// Run something to make the instance.img different with the initialized one.
TestResults origTestResults =
runVmTestService(
TAG,
vmOrig,
(ts, tr) -> {
tr.mAddInteger = ts.addInteger(123, 456);
tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
});
origTestResults.assertNoException();
assertThat(origTestResults.mAddInteger).isEqualTo(123 + 456);
VirtualMachineManager vmm = getVirtualMachineManager();
if (vmm.get(vmNameImport) != null) {
vmm.delete(vmNameImport);
}
// Action
VirtualMachine vmImport = vmm.importFromDescriptor(vmNameImport, vmOrig.toDescriptor());
// Asserts
assertFileContentsAreEqualInTwoVms("config.xml", vmNameOrig, vmNameImport);
assertFileContentsAreEqualInTwoVms("instance.img", vmNameOrig, vmNameImport);
if (encryptedStoreEnabled) {
assertFileContentsAreEqualInTwoVms("storage.img", vmNameOrig, vmNameImport);
}
assertThat(vmImport).isNotEqualTo(vmOrig);
vmm.delete(vmNameOrig);
assertThat(vmImport).isEqualTo(vmm.get(vmNameImport));
TestResults testResults =
runVmTestService(
TAG,
vmImport,
(ts, tr) -> {
tr.mAddInteger = ts.addInteger(123, 456);
tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
});
testResults.assertNoException();
assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
return testResults;
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void encryptedStorageAvailable() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setEncryptedStorageBytes(4_000_000)
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
});
assertThat(testResults.mEncryptedStoragePath).isEqualTo("/mnt/encryptedstore");
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void encryptedStorageIsInaccessibleToDifferentVm() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setEncryptedStorageBytes(4_000_000)
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
ts.writeToFile(
/* content= */ EXAMPLE_STRING,
/* path= */ "/mnt/encryptedstore/test_file");
});
testResults.assertNoException();
// Start a different vm (this changes the vm identity)
VirtualMachine diff_test_vm = forceCreateNewVirtualMachine("diff_test_vm", config);
// Replace the backing storage image to the original one
File storageImgOrig = getVmFile("test_vm", "storage.img");
File storageImgNew = getVmFile("diff_test_vm", "storage.img");
Files.copy(storageImgOrig.toPath(), storageImgNew.toPath(), REPLACE_EXISTING);
assertFileContentsAreEqualInTwoVms("storage.img", "test_vm", "diff_test_vm");
CompletableFuture<Boolean> onPayloadReadyExecuted = new CompletableFuture<>();
CompletableFuture<Boolean> onErrorExecuted = new CompletableFuture<>();
CompletableFuture<String> errorMessage = new CompletableFuture<>();
VmEventListener listener =
new VmEventListener() {
@Override
public void onPayloadReady(VirtualMachine vm) {
onPayloadReadyExecuted.complete(true);
super.onPayloadReady(vm);
}
@Override
public void onError(VirtualMachine vm, int errorCode, String message) {
onErrorExecuted.complete(true);
errorMessage.complete(message);
super.onError(vm, errorCode, message);
}
};
listener.runToFinish(TAG, diff_test_vm);
// Assert that payload never started & error message reflects storage error.
assertThat(onPayloadReadyExecuted.getNow(false)).isFalse();
assertThat(onErrorExecuted.getNow(false)).isTrue();
assertThat(errorMessage.getNow("")).contains("Unable to prepare encrypted storage");
}
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
public void microdroidLauncherHasEmptyCapabilities() throws Exception {
assumeSupportedDevice();
final VirtualMachineConfig vmConfig =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_caps", vmConfig);
final TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mEffectiveCapabilities = ts.getEffectiveCapabilities();
});
testResults.assertNoException();
assertThat(testResults.mEffectiveCapabilities).isEmpty();
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void payloadIsNotRoot() throws Exception {
assumeSupportedDevice();
assumeFeatureEnabled(VirtualMachineManager.FEATURE_MULTI_TENANT);
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mUid = ts.getUid();
});
testResults.assertNoException();
assertThat(testResults.mUid).isNotEqualTo(0);
}
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void encryptedStorageIsPersistent() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setEncryptedStorageBytes(4_000_000)
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_a", config);
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
ts.writeToFile(
/* content= */ EXAMPLE_STRING,
/* path= */ "/mnt/encryptedstore/test_file");
});
testResults.assertNoException();
// Re-run the same VM & verify the file persisted. Note, the previous `runVmTestService`
// stopped the VM
testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/test_file");
});
testResults.assertNoException();
assertThat(testResults.mFileContent).isEqualTo(EXAMPLE_STRING);
}
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
public void canReadFileFromAssets_debugFull() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_read_from_assets", config);
TestResults testResults =
runVmTestService(
TAG,
vm,
(testService, ts) -> {
ts.mFileContent = testService.readFromFile("/mnt/apk/assets/file.txt");
});
testResults.assertNoException();
assertThat(testResults.mFileContent).isEqualTo("Hello, I am a file!");
}
@Test
public void outputShouldBeExplicitlyCaptured() throws Exception {
assumeSupportedDevice();
final VirtualMachineConfig vmConfig =
new VirtualMachineConfig.Builder(getContext())
.setProtectedVm(mProtectedVm)
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.setVmConsoleInputSupported(true) // even if console input is supported
.build();
final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_forward_log", vmConfig);
vm.run();
try {
assertThrowsVmExceptionContaining(
() -> vm.getConsoleOutput(), "Capturing vm outputs is turned off");
assertThrowsVmExceptionContaining(
() -> vm.getLogOutput(), "Capturing vm outputs is turned off");
} finally {
vm.stop();
}
}
@Test
public void inputShouldBeExplicitlyAllowed() throws Exception {
assumeSupportedDevice();
final VirtualMachineConfig vmConfig =
new VirtualMachineConfig.Builder(getContext())
.setProtectedVm(mProtectedVm)
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.setVmOutputCaptured(true) // even if output is captured
.build();
final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_forward_log", vmConfig);
vm.run();
try {
assertThrowsVmExceptionContaining(
() -> vm.getConsoleInput(), "VM console input is not supported");
} finally {
vm.stop();
}
}
private boolean checkVmOutputIsRedirectedToLogcat(boolean debuggable) throws Exception {
String time =
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
final VirtualMachineConfig vmConfig =
new VirtualMachineConfig.Builder(getContext())
.setProtectedVm(mProtectedVm)
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(debuggable ? DEBUG_LEVEL_FULL : DEBUG_LEVEL_NONE)
.setVmOutputCaptured(false)
.build();
final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_logcat", vmConfig);
runVmTestService(TAG, vm, (service, results) -> {});
// only check logs printed after this test
Process logcatProcess =
new ProcessBuilder()
.command(
"logcat",
"-e",
"virtualizationmanager::aidl: Log.*executing main task",
"-t",
time)
.start();
logcatProcess.waitFor();
BufferedReader reader =
new BufferedReader(new InputStreamReader(logcatProcess.getInputStream()));
return !Strings.isNullOrEmpty(reader.readLine());
}
@Test
public void outputIsRedirectedToLogcatIfNotCaptured() throws Exception {
assumeSupportedDevice();
assertThat(checkVmOutputIsRedirectedToLogcat(true)).isTrue();
}
private boolean setSystemProperties(String name, String value) {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
UiAutomation uiAutomation = instrumentation.getUiAutomation();
String cmd = "setprop " + name + " " + (value.isEmpty() ? "\"\"" : value);
return runInShellWithStderr(TAG, uiAutomation, cmd).trim().isEmpty();
}
@Test
public void outputIsNotRedirectedToLogcatIfNotDebuggable() throws Exception {
assumeSupportedDevice();
// Disable debug policy to ensure no log output.
String sysprop = "hypervisor.virtualizationmanager.debug_policy.path";
String old = SystemProperties.get(sysprop);
assumeTrue(
"Can't disable debug policy. Perhapse user build?",
setSystemProperties(sysprop, ""));
try {
assertThat(checkVmOutputIsRedirectedToLogcat(false)).isFalse();
} finally {
assertThat(setSystemProperties(sysprop, old)).isTrue();
}
}
@Test
public void testConsoleInputSupported() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.setVmConsoleInputSupported(true)
.setVmOutputCaptured(true)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_console_in", config);
final String TYPED = "this is a console input\n";
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
OutputStreamWriter consoleIn =
new OutputStreamWriter(vm.getConsoleInput());
consoleIn.write(TYPED);
consoleIn.close();
tr.mConsoleInput = ts.readLineFromConsole();
});
testResults.assertNoException();
assertThat(testResults.mConsoleInput).isEqualTo(TYPED);
}
@Test
public void testStartVmWithPayloadOfAnotherApp() throws Exception {
assumeSupportedDevice();
Context ctx = getContext();
Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
VirtualMachineConfig config =
new VirtualMachineConfig.Builder(otherAppCtx)
.setDebugLevel(DEBUG_LEVEL_FULL)
.setProtectedVm(isProtectedVm())
.setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
.build();
try (VirtualMachine vm = forceCreateNewVirtualMachine("vm_from_another_app", config)) {
TestResults results =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mAddInteger = ts.addInteger(101, 303);
});
assertThat(results.mAddInteger).isEqualTo(404);
}
getVirtualMachineManager().delete("vm_from_another_app");
}
@Test
public void testVmDescriptorParcelUnparcel_noTrustedStorage() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine originalVm = forceCreateNewVirtualMachine("original_vm", config);
// Just start & stop the VM.
runVmTestService(TAG, originalVm, (ts, tr) -> {});
// Now create the descriptor and manually parcel & unparcel it.
VirtualMachineDescriptor vmDescriptor = toParcelFromParcel(originalVm.toDescriptor());
if (getVirtualMachineManager().get("import_vm_from_unparceled") != null) {
getVirtualMachineManager().delete("import_vm_from_unparceled");
}
VirtualMachine importVm =
getVirtualMachineManager()
.importFromDescriptor("import_vm_from_unparceled", vmDescriptor);
assertFileContentsAreEqualInTwoVms(
"config.xml", "original_vm", "import_vm_from_unparceled");
assertFileContentsAreEqualInTwoVms(
"instance.img", "original_vm", "import_vm_from_unparceled");
// Check that we can start and stop imported vm as well
runVmTestService(TAG, importVm, (ts, tr) -> {});
}
@Test
public void testVmDescriptorParcelUnparcel_withTrustedStorage() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.setEncryptedStorageBytes(1_000_000)
.build();
VirtualMachine originalVm = forceCreateNewVirtualMachine("original_vm", config);
// Just start & stop the VM.
{
TestResults testResults =
runVmTestService(
TAG,
originalVm,
(ts, tr) -> {
ts.writeToFile("not a secret!", "/mnt/encryptedstore/secret.txt");
});
assertThat(testResults.mException).isNull();
}
// Now create the descriptor and manually parcel & unparcel it.
VirtualMachineDescriptor vmDescriptor = toParcelFromParcel(originalVm.toDescriptor());
if (getVirtualMachineManager().get("import_vm_from_unparceled") != null) {
getVirtualMachineManager().delete("import_vm_from_unparceled");
}
VirtualMachine importVm =
getVirtualMachineManager()
.importFromDescriptor("import_vm_from_unparceled", vmDescriptor);
assertFileContentsAreEqualInTwoVms(
"config.xml", "original_vm", "import_vm_from_unparceled");
assertFileContentsAreEqualInTwoVms(
"instance.img", "original_vm", "import_vm_from_unparceled");
assertFileContentsAreEqualInTwoVms(
"storage.img", "original_vm", "import_vm_from_unparceled");
TestResults testResults =
runVmTestService(
TAG,
importVm,
(ts, tr) -> {
tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/secret.txt");
});
assertThat(testResults.mException).isNull();
assertThat(testResults.mFileContent).isEqualTo("not a secret!");
}
@Test
public void testShareVmWithAnotherApp() throws Exception {
assumeSupportedDevice();
Context ctx = getContext();
Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
VirtualMachineConfig config =
new VirtualMachineConfig.Builder(otherAppCtx)
.setDebugLevel(DEBUG_LEVEL_FULL)
.setProtectedVm(isProtectedVm())
.setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config);
// Just start & stop the VM.
runVmTestService(TAG, vm, (ts, tr) -> {});
// Get a descriptor that we will share with another app (VM_SHARE_APP_PACKAGE_NAME)
VirtualMachineDescriptor vmDesc = vm.toDescriptor();
Intent serviceIntent = new Intent();
serviceIntent.setComponent(
new ComponentName(
VM_SHARE_APP_PACKAGE_NAME,
"com.android.microdroid.test.sharevm.VmShareServiceImpl"));
serviceIntent.setAction("com.android.microdroid.test.sharevm.VmShareService");
VmShareServiceConnection connection = new VmShareServiceConnection();
boolean ret = ctx.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE);
assertWithMessage("Failed to bind to " + serviceIntent).that(ret).isTrue();
IVmShareTestService service = connection.waitForService();
assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
try {
// Send the VM descriptor to the other app. When received, it will reconstruct the VM
// from the descriptor, start it, connect to the ITestService in it, creates a "proxy"
// ITestService binder that delegates all the calls to the VM, and share it with this
// app. It will allow us to verify assertions on the running VM in the other app.
ITestService testServiceProxy = service.startVm(vmDesc);
int result = testServiceProxy.addInteger(37, 73);
assertThat(result).isEqualTo(110);
} finally {
ctx.unbindService(connection);
}
}
@Test
public void testShareVmWithAnotherApp_encryptedStorage() throws Exception {
assumeSupportedDevice();
Context ctx = getContext();
Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
VirtualMachineConfig config =
new VirtualMachineConfig.Builder(otherAppCtx)
.setDebugLevel(DEBUG_LEVEL_FULL)
.setProtectedVm(isProtectedVm())
.setEncryptedStorageBytes(3_000_000)
.setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config);
// Just start & stop the VM.
runVmTestService(
TAG,
vm,
(ts, tr) -> {
ts.writeToFile(EXAMPLE_STRING, "/mnt/encryptedstore/private.key");
});
// Get a descriptor that we will share with another app (VM_SHARE_APP_PACKAGE_NAME)
VirtualMachineDescriptor vmDesc = vm.toDescriptor();
Intent serviceIntent = new Intent();
serviceIntent.setComponent(
new ComponentName(
VM_SHARE_APP_PACKAGE_NAME,
"com.android.microdroid.test.sharevm.VmShareServiceImpl"));
serviceIntent.setAction("com.android.microdroid.test.sharevm.VmShareService");
VmShareServiceConnection connection = new VmShareServiceConnection();
boolean ret = ctx.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE);
assertWithMessage("Failed to bind to " + serviceIntent).that(ret).isTrue();
IVmShareTestService service = connection.waitForService();
assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
try {
// Send the VM descriptor to the other app. When received, it will reconstruct the VM
// from the descriptor, start it, connect to the ITestService in it, creates a "proxy"
// ITestService binder that delegates all the calls to the VM, and share it with this
// app. It will allow us to verify assertions on the running VM in the other app.
ITestService testServiceProxy = service.startVm(vmDesc);
String result = testServiceProxy.readFromFile("/mnt/encryptedstore/private.key");
assertThat(result).isEqualTo(EXAMPLE_STRING);
} finally {
ctx.unbindService(connection);
}
}
@Test
@CddTest(requirements = {"9.17/C-1-5"})
public void testFileUnderBinHasExecutePermission() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig vmConfig =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_perms", vmConfig);
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mFileMode = ts.getFilePermissions("/mnt/apk/bin/measure_io");
});
testResults.assertNoException();
int allPermissionsMask =
OsConstants.S_IRUSR
| OsConstants.S_IWUSR
| OsConstants.S_IXUSR
| OsConstants.S_IRGRP
| OsConstants.S_IWGRP
| OsConstants.S_IXGRP
| OsConstants.S_IROTH
| OsConstants.S_IWOTH
| OsConstants.S_IXOTH;
int expectedPermissions = OsConstants.S_IRUSR | OsConstants.S_IXUSR;
if (isFeatureEnabled(VirtualMachineManager.FEATURE_MULTI_TENANT)) {
expectedPermissions |= OsConstants.S_IRGRP | OsConstants.S_IXGRP;
}
assertThat(testResults.mFileMode & allPermissionsMask).isEqualTo(expectedPermissions);
}
// Taken from bionic/libc/kernel/uapi/linux/mount.h
private static final int MS_RDONLY = 1;
private static final int MS_NOEXEC = 8;
private static final int MS_NOATIME = 1024;
@Test
@CddTest(requirements = {"9.17/C-1-5"})
public void dataIsMountedWithNoExec() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig vmConfig =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_data_mount", vmConfig);
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mMountFlags = ts.getMountFlags("/data");
});
assertThat(testResults.mException).isNull();
assertWithMessage("/data should be mounted with MS_NOEXEC")
.that(testResults.mMountFlags & MS_NOEXEC)
.isEqualTo(MS_NOEXEC);
}
@Test
@CddTest(requirements = {"9.17/C-1-5"})
public void encryptedStoreIsMountedWithNoExec() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig vmConfig =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.setEncryptedStorageBytes(4_000_000)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_encstore_no_exec", vmConfig);
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mMountFlags = ts.getMountFlags("/mnt/encryptedstore");
});
assertThat(testResults.mException).isNull();
assertWithMessage("/mnt/encryptedstore should be mounted with MS_NOEXEC")
.that(testResults.mMountFlags & MS_NOEXEC)
.isEqualTo(MS_NOEXEC);
}
@Test
@VsrTest(requirements = {"VSR-7.1-001.003"})
public void kernelVersionRequirement() throws Exception {
int firstApiLevel = SystemProperties.getInt("ro.product.first_api_level", 0);
assume().withMessage("Skip on devices launched before Android 14 (API level 34)")
.that(firstApiLevel)
.isAtLeast(34);
String[] tokens = KERNEL_VERSION.split("\\.");
int major = Integer.parseInt(tokens[0]);
int minor = Integer.parseInt(tokens[1]);
// Check kernel version >= 5.15
assertTrue(major >= 5);
if (major == 5) {
assertTrue(minor >= 15);
}
}
@Test
public void configuringVendorDiskImageRequiresCustomPermission() throws Exception {
assumeSupportedDevice();
assumeFeatureEnabled(VirtualMachineManager.FEATURE_VENDOR_MODULES);
File vendorDiskImage =
new File("/data/local/tmp/cts/microdroid/test_microdroid_vendor_image.img");
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setVendorDiskImage(vendorDiskImage)
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm =
forceCreateNewVirtualMachine("test_vendor_image_req_custom_permission", config);
SecurityException e =
assertThrows(
SecurityException.class, () -> runVmTestService(TAG, vm, (ts, tr) -> {}));
assertThat(e)
.hasMessageThat()
.contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission");
}
@Test
public void bootsWithVendorPartition() throws Exception {
assumeSupportedDevice();
assumeFeatureEnabled(VirtualMachineManager.FEATURE_VENDOR_MODULES);
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
File vendorDiskImage =
new File("/data/local/tmp/cts/microdroid/test_microdroid_vendor_image.img");
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setVendorDiskImage(vendorDiskImage)
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_boot_with_vendor", config);
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mMountFlags = ts.getMountFlags("/vendor");
});
assertThat(testResults.mException).isNull();
int expectedFlags = MS_NOATIME | MS_RDONLY;
assertThat(testResults.mMountFlags & expectedFlags).isEqualTo(expectedFlags);
}
@Test
public void creationFailsWithUnsignedVendorPartition() throws Exception {
assumeSupportedDevice();
assumeFeatureEnabled(VirtualMachineManager.FEATURE_VENDOR_MODULES);
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
File unsignedVendorDiskImage =
new File(
"/data/local/tmp/cts/microdroid/test_microdroid_vendor_image_unsigned.img");
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setVendorDiskImage(unsignedVendorDiskImage)
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_boot_with_unsigned_vendor", config);
assertThrowsVmExceptionContaining(
() -> vm.run(), "Failed to get vbmeta from microdroid-vendor.img");
}
@Test
public void systemPartitionMountFlags() throws Exception {
assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_system_mount_flags", config);
TestResults testResults =
runVmTestService(
TAG,
vm,
(ts, tr) -> {
tr.mMountFlags = ts.getMountFlags("/");
});
assertThat(testResults.mException).isNull();
int expectedFlags = MS_NOATIME | MS_RDONLY;
assertThat(testResults.mMountFlags & expectedFlags).isEqualTo(expectedFlags);
}
private static class VmShareServiceConnection implements ServiceConnection {
private final CountDownLatch mLatch = new CountDownLatch(1);
private IVmShareTestService mVmShareTestService;
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mVmShareTestService = IVmShareTestService.Stub.asInterface(service);
mLatch.countDown();
}
@Override
public void onServiceDisconnected(ComponentName name) {}
private IVmShareTestService waitForService() throws Exception {
if (!mLatch.await(1, TimeUnit.MINUTES)) {
return null;
}
return mVmShareTestService;
}
}
private VirtualMachineDescriptor toParcelFromParcel(VirtualMachineDescriptor descriptor) {
Parcel parcel = Parcel.obtain();
descriptor.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
return VirtualMachineDescriptor.CREATOR.createFromParcel(parcel);
}
private void assertFileContentsAreEqualInTwoVms(String fileName, String vmName1, String vmName2)
throws IOException {
File file1 = getVmFile(vmName1, fileName);
File file2 = getVmFile(vmName2, fileName);
try (FileInputStream input1 = new FileInputStream(file1);
FileInputStream input2 = new FileInputStream(file2)) {
assertThat(Arrays.equals(input1.readAllBytes(), input2.readAllBytes())).isTrue();
}
}
private File getVmFile(String vmName, String fileName) {
Context context = getContext();
Path filePath = Paths.get(context.getDataDir().getPath(), "vm", vmName, fileName);
return filePath.toFile();
}
private void assertThrowsVmException(ThrowingRunnable runnable) {
assertThrows(VirtualMachineException.class, runnable);
}
private void assertThrowsVmExceptionContaining(
ThrowingRunnable runnable, String expectedContents) {
Exception e = assertThrows(VirtualMachineException.class, runnable);
assertThat(e).hasMessageThat().contains(expectedContents);
}
private long minMemoryRequired() {
if (Build.SUPPORTED_ABIS.length > 0) {
String primaryAbi = Build.SUPPORTED_ABIS[0];
switch (primaryAbi) {
case "x86_64":
return MIN_MEM_X86_64;
case "arm64-v8a":
return MIN_MEM_ARM64;
}
}
return 0;
}
private void assumeSupportedDevice() {
assume()
.withMessage("Skip on 5.4 kernel. b/218303240")
.that(KERNEL_VERSION)
.isNotEqualTo("5.4");
}
}