Add APIs for creating and running a virtual machine
Bug: 183496040
Test: atest MicrodroidHostTestCase
Test: TARGET_BUILD_APPS=MicrodroidTestApp.signed
Push the build MicrodroidTestApp.apk.idsig to /data/local/tmp/virt
Install the APK and run it.
Change-Id: I1614f8c83605a32ef34b62216c13cb4e4f7fcf99
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 504bc03..8089d85 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -16,5 +16,267 @@
package android.system.virtualmachine;
-/** @hide */
-public class VirtualMachine {}
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.system.virtualizationservice.IVirtualMachine;
+import android.system.virtualizationservice.IVirtualizationService;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Optional;
+
+/**
+ * A handle to the virtual machine. The virtual machine is local to the app which created the
+ * virtual machine.
+ *
+ * @hide
+ */
+public class VirtualMachine {
+ /** Name of the directory under the files directory where all VMs created for the app exist. */
+ private static final String VM_DIR = "vm";
+
+ /** Name of the persisted config file for a VM. */
+ private static final String CONFIG_FILE = "config.xml";
+
+ /** Name of the instance image file for a VM. (Not implemented) */
+ private static final String INSTANCE_IMAGE_FILE = "instance.img";
+
+ /** Name of the virtualization service. */
+ private static final String SERVICE_NAME = "android.system.virtualizationservice";
+
+ /** Status of a virtual machine */
+ public enum Status {
+ /** The virtual machine has just been created, or {@link #stop()} was called on it. */
+ STOPPED,
+ /** The virtual machine is running. */
+ RUNNING,
+ /**
+ * The virtual machine is deleted. This is a irreversable state. Once a virtual machine is
+ * deleted, it can never be undone, which means all its secrets are permanently lost.
+ */
+ DELETED,
+ }
+
+ /** The package which owns this VM. */
+ private final String mPackageName;
+
+ /** Name of this VM within the package. The name should be unique in the package. */
+ private final String mName;
+
+ /**
+ * Path to the config file for this VM. The config file is where the configuration is persisted.
+ */
+ private final File mConfigFilePath;
+
+ /** Path to the instance image file for this VM. (Not implemented) */
+ private final File mInstanceFilePath;
+
+ /** The configuration that is currently associated with this VM. */
+ private VirtualMachineConfig mConfig;
+
+ /** Handle to the "running" VM. */
+ private IVirtualMachine mVirtualMachine;
+
+ private ParcelFileDescriptor mConsoleReader;
+ private ParcelFileDescriptor mConsoleWriter;
+
+ private VirtualMachine(Context context, String name, VirtualMachineConfig config) {
+ mPackageName = context.getPackageName();
+ mName = name;
+ mConfig = config;
+
+ final File vmRoot = new File(context.getFilesDir(), VM_DIR);
+ final File thisVmDir = new File(vmRoot, mName);
+ mConfigFilePath = new File(thisVmDir, CONFIG_FILE);
+ mInstanceFilePath = new File(thisVmDir, INSTANCE_IMAGE_FILE);
+ }
+
+ /**
+ * Creates a virtual machine with the given name and config. Once a virtual machine is created
+ * it is persisted until it is deleted by calling {@link #delete()}. The created virtual machine
+ * is in {@link #STOPPED} state. To run the VM, call {@link #run()}.
+ */
+ /* package */ static VirtualMachine create(
+ Context context, String name, VirtualMachineConfig config)
+ throws VirtualMachineException {
+ // TODO(jiyong): trigger an error if the VM having 'name' already exists.
+ VirtualMachine vm = new VirtualMachine(context, name, config);
+
+ try {
+ final File vmRoot = vm.mConfigFilePath.getParentFile();
+ Files.createDirectories(vmRoot.toPath());
+
+ FileOutputStream output = new FileOutputStream(vm.mConfigFilePath);
+ vm.mConfig.serialize(output);
+ output.close();
+ } catch (IOException e) {
+ throw new VirtualMachineException(e);
+ }
+
+ // TODO(jiyong): create the instance image file
+ return vm;
+ }
+
+ /** Loads a virtual machine that is already created before. */
+ /* package */ static VirtualMachine load(Context context, String name)
+ throws VirtualMachineException {
+ // TODO(jiyong): return null if the VM having the 'name' doesn't exist.
+ VirtualMachine vm = new VirtualMachine(context, name, /* config */ null);
+
+ try {
+ FileInputStream input = new FileInputStream(vm.mConfigFilePath);
+ VirtualMachineConfig config = VirtualMachineConfig.from(input);
+ input.close();
+ vm.mConfig = config;
+ } catch (IOException e) {
+ throw new VirtualMachineException(e);
+ }
+
+ return vm;
+ }
+
+ /**
+ * Returns the name of this virtual machine. The name is unique in the package and can't be
+ * changed.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the currently selected config of this virtual machine. There can be multiple virtual
+ * machines sharing the same config. Even in that case, the virtual machines are completely
+ * isolated from each other; one cannot share its secret to another virtual machine even if they
+ * share the same config. It is also possible that a virtual machine can switch its config,
+ * which can be done by calling {@link #setConfig(VirtualMachineCOnfig)}.
+ */
+ public VirtualMachineConfig getConfig() {
+ return mConfig;
+ }
+
+ /** Returns the current status of this virtual machine. */
+ public Status getStatus() throws VirtualMachineException {
+ try {
+ if (mVirtualMachine != null && mVirtualMachine.isRunning()) {
+ return Status.RUNNING;
+ }
+ } catch (RemoteException e) {
+ throw new VirtualMachineException(e);
+ }
+ if (!mConfigFilePath.exists()) {
+ return Status.DELETED;
+ }
+ return Status.STOPPED;
+ }
+
+ /**
+ * Runs this virtual machine. The returning of this method however doesn't mean that the VM has
+ * actually started running or the OS has booted there. Such events can be notified by
+ * registering a callback object (not implemented currently).
+ */
+ public void run() throws VirtualMachineException {
+ if (getStatus() != Status.STOPPED) {
+ throw new VirtualMachineException(this + " is not in stopped state");
+ }
+ IVirtualizationService service =
+ IVirtualizationService.Stub.asInterface(ServiceManager.getService(SERVICE_NAME));
+
+ try {
+ if (mConsoleReader == null && mConsoleWriter == null) {
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ mConsoleReader = pipe[0];
+ mConsoleWriter = pipe[1];
+ }
+ mVirtualMachine =
+ service.startVm(
+ android.system.virtualizationservice.VirtualMachineConfig.appConfig(
+ getConfig().toParcel()),
+ mConsoleWriter);
+ } catch (IOException e) {
+ throw new VirtualMachineException(e);
+ } catch (RemoteException e) {
+ throw new VirtualMachineException(e);
+ }
+ }
+
+ /** Returns the stream object representing the console output from the virtual machine. */
+ public InputStream getConsoleOutputStream() throws VirtualMachineException {
+ if (mConsoleReader == null) {
+ throw new VirtualMachineException("Console output not available");
+ }
+ return new FileInputStream(mConsoleReader.getFileDescriptor());
+ }
+
+ /**
+ * Stops this virtual machine. Stopping a virtual machine is like pulling the plug on a real
+ * computer; the machine halts immediately. Software running on the virtual machine is not
+ * notified with the event. A stopped virtual machine can be re-started by calling {@link
+ * #run()}.
+ */
+ public void stop() throws VirtualMachineException {
+ // Dropping the IVirtualMachine handle stops the VM
+ mVirtualMachine = null;
+ }
+
+ /**
+ * Deletes this virtual machine. Deleting a virtual machine means deleting any persisted data
+ * associated with it including the per-VM secret. This is an irreversable action. A virtual
+ * machine once deleted can never be restored. A new virtual machine created with the same name
+ * and the same config is different from an already deleted virtual machine.
+ */
+ public void delete() throws VirtualMachineException {
+ if (getStatus() != Status.STOPPED) {
+ throw new VirtualMachineException("Virtual machine is not stopped");
+ }
+ final File vmRootDir = mConfigFilePath.getParentFile();
+ mConfigFilePath.delete();
+ mInstanceFilePath.delete();
+ vmRootDir.delete();
+ }
+
+ /** Returns the CID of this virtual machine, if it is running. */
+ public Optional<Integer> getCid() throws VirtualMachineException {
+ if (getStatus() != Status.RUNNING) {
+ return Optional.empty();
+ }
+ try {
+ return Optional.of(mVirtualMachine.getCid());
+ } catch (RemoteException e) {
+ throw new VirtualMachineException(e);
+ }
+ }
+
+ /**
+ * Changes the config of this virtual machine to a new one. This can be used to adjust things
+ * like the number of CPU and size of the RAM, depending on the situation (e.g. the size of the
+ * application to run on the virtual machine, etc.) However, changing a config might make the
+ * virtual machine un-bootable if the new config is not compatible with the existing one. For
+ * example, if the signer of the app payload in the new config is different from that of the old
+ * config, the virtual machine won't boot. To prevent such cases, this method returns exception
+ * when an incompatible config is attempted.
+ *
+ * @return the old config
+ */
+ public VirtualMachineConfig setConfig(VirtualMachineConfig newConfig)
+ throws VirtualMachineException {
+ // TODO(jiyong): implement this
+ throw new VirtualMachineException("Not implemented");
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("VirtualMachine(");
+ sb.append("name:" + getName() + ", ");
+ sb.append("config:" + getConfig().getPayloadConfigPath() + ", ");
+ sb.append("package: " + mPackageName);
+ sb.append(")");
+ return sb.toString();
+ }
+}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
new file mode 100644
index 0000000..062cd04
--- /dev/null
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -0,0 +1,143 @@
+/*
+ * 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 android.system.virtualmachine;
+
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.system.virtualizationservice.VirtualMachineAppConfig;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Represents a configuration of a virtual machine. A configuration consists of hardware
+ * configurations like the number of CPUs and the size of RAM, and software configurations like the
+ * OS and application to run on the virtual machine.
+ *
+ * @hide
+ */
+public final class VirtualMachineConfig {
+ // These defines the schema of the config file persisted on disk.
+ private static final int VERSION = 1;
+ private static final String KEY_VERSION = "version";
+ private static final String KEY_APKPATH = "apkPath";
+ private static final String KEY_IDSIGPATH = "idsigPath";
+ private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath";
+
+ // Paths to the APK and its idsig file of this application.
+ private final String mApkPath;
+ private final String mIdsigPath;
+
+ /**
+ * Path within the APK to the payload config file that defines software aspects of this config.
+ */
+ private final String mPayloadConfigPath;
+
+ // TODO(jiyong): add more items like # of cpu, size of ram, debuggability, etc.
+
+ private VirtualMachineConfig(String apkPath, String idsigPath, String payloadConfigPath) {
+ mApkPath = apkPath;
+ mIdsigPath = idsigPath;
+ mPayloadConfigPath = payloadConfigPath;
+ }
+
+ /** Loads a config from a stream, for example a file. */
+ /* package */ static VirtualMachineConfig from(InputStream input)
+ throws IOException, VirtualMachineException {
+ PersistableBundle b = PersistableBundle.readFromStream(input);
+ final int version = b.getInt(KEY_VERSION);
+ if (version > VERSION) {
+ throw new VirtualMachineException("Version too high");
+ }
+ final String apkPath = b.getString(KEY_APKPATH);
+ if (apkPath == null) {
+ throw new VirtualMachineException("No apkPath");
+ }
+ final String idsigPath = b.getString(KEY_IDSIGPATH);
+ if (idsigPath == null) {
+ throw new VirtualMachineException("No idsigPath");
+ }
+ final String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH);
+ if (payloadConfigPath == null) {
+ throw new VirtualMachineException("No payloadConfigPath");
+ }
+ return new VirtualMachineConfig(apkPath, idsigPath, payloadConfigPath);
+ }
+
+ /** Persists this config to a stream, for example a file. */
+ /* package */ void serialize(OutputStream output) throws IOException {
+ PersistableBundle b = new PersistableBundle();
+ b.putInt(KEY_VERSION, VERSION);
+ b.putString(KEY_APKPATH, mApkPath);
+ b.putString(KEY_IDSIGPATH, mIdsigPath);
+ b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath);
+ b.writeToStream(output);
+ }
+
+ /** Returns the path to the payload config within the owning application. */
+ public String getPayloadConfigPath() {
+ return mPayloadConfigPath;
+ }
+
+ /**
+ * Converts this config object into a parcel. Used when creating a VM via the virtualization
+ * service. Notice that the files are not passed as paths, but as file descriptors because the
+ * service doesn't accept paths as it might not have permission to open app-owned files and that
+ * could be abused to run a VM with software that the calling application doesn't own.
+ */
+ /* package */ VirtualMachineAppConfig toParcel() throws FileNotFoundException {
+ VirtualMachineAppConfig parcel = new VirtualMachineAppConfig();
+ parcel.apk = ParcelFileDescriptor.open(new File(mApkPath), MODE_READ_ONLY);
+ parcel.idsig = ParcelFileDescriptor.open(new File(mIdsigPath), MODE_READ_ONLY);
+ parcel.configPath = mPayloadConfigPath;
+ return parcel;
+ }
+
+ /** A builder used to create a {@link VirtualMachineConfig}. */
+ public static class Builder {
+ private Context mContext;
+ private String mPayloadConfigPath;
+ private String mIdsigPath; // TODO(jiyong): remove this
+ // TODO(jiyong): add more items like # of cpu, size of ram, debuggability, etc.
+
+ /** Creates a builder for the given context (APK), and the payload config file in APK. */
+ public Builder(Context context, String payloadConfigPath) {
+ mContext = context;
+ mPayloadConfigPath = payloadConfigPath;
+ }
+
+ // TODO(jiyong): remove this. Apps shouldn't need to set the path to the idsig file. It
+ // should be automatically found or created on demand.
+ /** Set the path to the idsig file for the current application. */
+ public Builder idsigPath(String idsigPath) {
+ mIdsigPath = idsigPath;
+ return this;
+ }
+
+ /** Builds an immutable {@link VirtualMachineConfig} */
+ public VirtualMachineConfig build() {
+ final String apkPath = mContext.getPackageCodePath();
+ return new VirtualMachineConfig(apkPath, mIdsigPath, mPayloadConfigPath);
+ }
+ }
+}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineException.java b/javalib/src/android/system/virtualmachine/VirtualMachineException.java
new file mode 100644
index 0000000..d6aeab3
--- /dev/null
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineException.java
@@ -0,0 +1,36 @@
+/*
+ * 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 android.system.virtualmachine;
+
+/** @hide */
+public class VirtualMachineException extends Exception {
+ public VirtualMachineException() {
+ super();
+ }
+
+ public VirtualMachineException(String message) {
+ super(message);
+ }
+
+ public VirtualMachineException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public VirtualMachineException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
new file mode 100644
index 0000000..dfa4f0b
--- /dev/null
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -0,0 +1,94 @@
+/*
+ * 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 android.system.virtualmachine;
+
+import android.content.Context;
+
+import java.lang.ref.WeakReference;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * Manages {@link VirtualMachine} objects created for an application.
+ *
+ * @hide
+ */
+public class VirtualMachineManager {
+ private final Context mContext;
+
+ private VirtualMachineManager(Context context) {
+ mContext = context;
+ }
+
+ static Map<Context, WeakReference<VirtualMachineManager>> sInstances = new WeakHashMap<>();
+
+ /** Returns the per-context instance. */
+ public static VirtualMachineManager getInstance(Context context) {
+ synchronized (sInstances) {
+ VirtualMachineManager vmm =
+ sInstances.containsKey(context) ? sInstances.get(context).get() : null;
+ if (vmm == null) {
+ vmm = new VirtualMachineManager(context);
+ sInstances.put(context, new WeakReference(vmm));
+ }
+ return vmm;
+ }
+ }
+
+ /** A lock used to synchronize the creation of virtual machines */
+ private static final Object sCreateLock = new Object();
+
+ /**
+ * Creates a new {@link VirtualMachine} with the given name and config. Creating a virtual
+ * machine with the same name as an existing virtual machine is an error. The existing virtual
+ * machine has to be deleted before its name can be reused. Every call to this methods creates a
+ * new (and different) virtual machine even if the name and the config are the same as the
+ * deleted one.
+ */
+ public VirtualMachine create(String name, VirtualMachineConfig config)
+ throws VirtualMachineException {
+ synchronized (sCreateLock) {
+ return VirtualMachine.create(mContext, name, config);
+ }
+ }
+
+ /**
+ * Returns an existing {@link VirtualMachine} with the given name. Returns null if there is no
+ * such virtual machine.
+ */
+ public VirtualMachine get(String name) throws VirtualMachineException {
+ return VirtualMachine.load(mContext, name);
+ }
+
+ /** Returns an existing {@link VirtualMachine} if it exists, or create a new one. */
+ public VirtualMachine getOrCreate(String name, VirtualMachineConfig config)
+ throws VirtualMachineException {
+ VirtualMachine vm;
+ synchronized (sCreateLock) {
+ vm = get(name);
+ if (vm == null) {
+ return create(name, config);
+ }
+ }
+
+ if (vm.getConfig().equals(config)) {
+ return vm;
+ } else {
+ throw new VirtualMachineException("Incompatible config");
+ }
+ }
+}
diff --git a/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java b/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java
index b25869b..f73772e 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java
@@ -17,8 +17,35 @@
import android.app.Activity;
import android.os.Bundle;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
public class TestActivity extends Activity {
+
@Override
- public void onCreate(Bundle savedInstanceState) {}
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ VirtualMachine vm1 = createAndRunVirtualMachine("vm1");
+ VirtualMachine vm2 = createAndRunVirtualMachine("vm2");
+ }
+
+ private VirtualMachine createAndRunVirtualMachine(String name) {
+ VirtualMachine vm;
+ try {
+ VirtualMachineConfig config =
+ new VirtualMachineConfig.Builder(this, "assets/vm_config.json")
+ .idsigPath("/data/local/tmp/virt/MicrodroidTestApp.apk.idsig")
+ .build();
+
+ VirtualMachineManager vmm = VirtualMachineManager.getInstance(this);
+ vm = vmm.create(name, config);
+ vm.run();
+ } catch (VirtualMachineException e) {
+ throw new RuntimeException(e);
+ }
+ return vm;
+ }
}
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index a3311f2..f7cb339 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -5,8 +5,8 @@
aidl_interface {
name: "android.system.virtualizationservice",
srcs: ["**/*.aidl"],
- // This is never accessed directly. Apps are expected to use this indirectly via the java wrapper
- // android.system.virtualmachine.
+ // This is never accessed directly. Apps are expected to use this indirectly via the Java
+ // wrapper android.system.virtualmachine.
unstable: true,
backend: {
java: {