Rename javalib/ to java and move APIs to java/framework
This follows a setup of other APEXes that contribute to BCP & SSCP.
Bug: 325196727
Test: builds
Change-Id: I6bc9d6628f28e9c880f09be475af9ba7c34a4ca3
diff --git a/java/Android.bp b/java/Android.bp
new file mode 100644
index 0000000..1c55f78
--- /dev/null
+++ b/java/Android.bp
@@ -0,0 +1,24 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+soong_config_module_type {
+ name: "avf_flag_aware_android_app",
+ module_type: "android_app",
+ config_namespace: "ANDROID",
+ bool_variables: ["release_avf_allow_preinstalled_apps"],
+ properties: ["manifest"],
+}
+
+// Defines our permissions
+avf_flag_aware_android_app {
+ name: "android.system.virtualmachine.res",
+ installable: true,
+ apex_available: ["com.android.virt"],
+ platform_apis: true,
+ soong_config_variables: {
+ release_avf_allow_preinstalled_apps: {
+ manifest: "AndroidManifestNext.xml",
+ },
+ },
+}
diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml
new file mode 100644
index 0000000..95b9cfa
--- /dev/null
+++ b/java/AndroidManifest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.virtualmachine.res">
+
+ <!-- @SystemApi Allows an application to create and run a Virtual Machine
+ using the Virtualization Framework APIs
+ (android.system.virtualmachine.*).
+ <p>Protection level: signature|privileged|development
+ @hide
+ -->
+ <permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE"
+ android:protectionLevel="signature|privileged|development" />
+
+ <!-- @hide Allows an application to run a Virtual Machine with a custom
+ kernel or a Microdroid configuration file.
+ <p>Not for use by third-party applications.
+ -->
+ <permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE"
+ android:protectionLevel="signature|development" />
+
+ <!-- @hide Allows an application to access various Virtual Machine debug
+ facilities, e.g. list all running VMs.
+ <p>Not for use by third-party applications.
+ -->
+ <permission android:name="android.permission.DEBUG_VIRTUAL_MACHINE"
+ android:protectionLevel="signature" />
+
+ <application android:hasCode="false" />
+</manifest>
diff --git a/java/AndroidManifestNext.xml b/java/AndroidManifestNext.xml
new file mode 100644
index 0000000..ebcb8ba
--- /dev/null
+++ b/java/AndroidManifestNext.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.virtualmachine.res">
+
+ <!-- @SystemApi Allows an application to create and run a Virtual Machine
+ using the Virtualization Framework APIs
+ (android.system.virtualmachine.*).
+ <p>Protection level: signature|preinstalled|development
+ @hide
+ -->
+ <permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE"
+ android:protectionLevel="signature|preinstalled|development" />
+
+ <!-- @hide Allows an application to run a Virtual Machine with a custom
+ kernel or a Microdroid configuration file.
+ <p>Not for use by third-party applications.
+ -->
+ <permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE"
+ android:protectionLevel="signature|development" />
+
+ <!-- @hide Allows an application to access various Virtual Machine debug
+ facilities, e.g. list all running VMs.
+ <p>Not for use by third-party applications.
+ -->
+ <permission android:name="android.permission.DEBUG_VIRTUAL_MACHINE"
+ android:protectionLevel="signature" />
+
+ <application android:hasCode="false" />
+</manifest>
diff --git a/java/framework/Android.bp b/java/framework/Android.bp
new file mode 100644
index 0000000..32b2aee
--- /dev/null
+++ b/java/framework/Android.bp
@@ -0,0 +1,46 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_sdk_library {
+ name: "framework-virtualization",
+
+ defaults: ["non-updatable-framework-module-defaults"],
+
+ jarjar_rules: "jarjar-rules.txt",
+
+ srcs: ["src/**/*.java"],
+ static_libs: [
+ "android.system.virtualizationservice-java",
+ "avf_aconfig_flags_java",
+ // For android.sysprop.HypervisorProperties
+ "PlatformProperties",
+ ],
+
+ apex_available: ["com.android.virt"],
+
+ permitted_packages: [
+ "android.system.virtualmachine",
+ "android.system.virtualizationservice",
+ // android.sysprop.*, renamed by jarjar
+ "com.android.system.virtualmachine.sysprop",
+ ],
+ errorprone: {
+ enabled: true,
+ javacflags: [
+ // We use @GuardedBy and we want a test failure if our locking isn't consistent with it.
+ "-Xep:GuardedBy:ERROR",
+ ],
+ },
+
+ sdk_version: "core_platform",
+ stub_only_libs: [
+ "android_module_lib_stubs_current",
+ ],
+ impl_only_libs: [
+ "framework",
+ ],
+ impl_library_visibility: [
+ "//packages/modules/Virtualization:__subpackages__",
+ ],
+}
diff --git a/java/framework/README.md b/java/framework/README.md
new file mode 100644
index 0000000..cf7a6cb
--- /dev/null
+++ b/java/framework/README.md
@@ -0,0 +1,371 @@
+# Android Virtualization Framework API
+
+These Java APIs allow an app to configure and run a Virtual Machine running
+[Microdroid](../microdroid/README.md) and execute native code from the app (the
+payload) within it.
+
+There is more information on AVF [here](../README.md). To see how to package the
+payload code that is to run inside a VM, and the native API available to it, see
+the [VM Payload API](../vm_payload/README.md)
+
+The API classes are all in the
+[`android.system.virtualmachine`](src/android/system/virtualmachine) package.
+
+Note that these APIs are all `@SystemApi` and require the restricted
+`android.permission.MANAGE_VIRTUAL_MACHINE` permission, so they are not
+available to third party apps.
+
+All of these APIs were introduced in API level 34 (Android 14). The classes may
+not exist in devices running an earlier version.
+
+## Detecting AVF Support
+
+The simplest way to detect whether a device has support for AVF is to retrieve
+an instance of the
+[`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java)
+class; if the result is not `null` then the device has support. You can then
+find out whether protected, non-protected VMs, or both are supported using the
+`getCapabilities()` method. Note that this code requires API level 34 or higher:
+
+```Java
+VirtualMachineManager vmm = context.getSystemService(VirtualMachineManager.class);
+if (vmm == null) {
+ // AVF is not supported.
+} else {
+ // AVF is supported.
+ int capabilities = vmm.getCapabilities();
+ if ((capabilties & CAPABILITY_PROTECTED_VM) != 0) {
+ // Protected VMs supported.
+ }
+ if ((capabilties & CAPABILITY_NON_PROTECTED_VM) != 0) {
+ // Non-Protected VMs supported.
+ }
+}
+```
+
+An alternative for detecting AVF support is to query support for the
+`android.software.virtualization_framework` system feature. This method will
+work on any API level, and return false if it is below 34:
+
+```Java
+if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK)) {
+ // AVF is supported.
+}
+```
+
+You can also express a dependency on this system feature in your app's manifest
+with a
+[`<uses-feature>`](https://developer.android.com/guide/topics/manifest/uses-feature-element)
+element.
+
+
+## Starting a VM
+
+Once you have an instance of the
+[`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java),
+a VM can be started by:
+- Specifying the desired VM configuration, using a
+ [`VirtualMachineConfig`](src/android/system/virtualmachine/VirtualMachineConfig.java)
+ builder;
+- Creating a new
+ [`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java)
+ instance (or retrieving an existing one);
+- Registering to retrieve events from the VM by providing a
+ [`VirtualMachineCallback`](src/android/system/virtualmachine/VirtualMachineCallback.java)
+ (optional, but recommended);
+- Running the VM.
+
+A minimal example might look like this:
+
+```Java
+VirtualMachineConfig config =
+ new VirtualMachineConfig.Builder(this)
+ .setProtectedVm(true)
+ .setPayloadBinaryName("my_payload.so")
+ .build();
+
+VirtualMachine vm = vmm.getOrCreate("my vm", config);
+
+vm.setCallback(executor, new VirtualMachineCallback() {...});
+
+vm.run();
+```
+
+Here we are running a protected VM, which will execute the code in the
+`my_payload.so` file included in your APK.
+
+Information about the VM, including its configuration, is stored in files in
+your app's private data directory. The file names are based on the VM name you
+supply. So once an instance of a VM has been created it can be retrieved by name
+even if the app is restarted or the device is rebooted. Directly inspecting or
+modifying these files is not recommended.
+
+The `getOrCreate()` call will retrieve an existing VM instance if it exists (in
+which case the `config` parameter is ignored), or create a new one
+otherwise. There are also separate `get()` and `create()` methods.
+
+The `run()` method is asynchronous; it returns successfully once the VM is
+starting. You can find out when the VM is ready, or if it fails, via your
+`VirtualMachineCallback` implementation.
+
+## VM Configuration
+
+There are other things that you can specify as part of the
+[`VirtualMachineConfig`](src/android/system/virtualmachine/VirtualMachineConfig.java):
+- Whether the VM should be debuggable. A debuggable VM is not secure, but it
+ does allow access to logs from inside the VM, which can be useful for
+ troubleshooting.
+- How much memory should be available to the VM. (This is an upper bound;
+ typically memory is allocated to the VM as it is needed until the limit is
+ reached - but there is some overhead proportional to the maximum size.)
+- How many virtual CPUs the VM has.
+- How much encrypted storage the VM has.
+- The path to the installed APK containing the code to run as the VM
+ payload. (Normally you don't need this; the APK path is determined from the
+ context passed to the config builder.)
+
+## VM Life-cycle
+
+To find out the progress of the Virtual Machine once it is started you should
+implement the methods defined by
+[`VirtualMachineCallback`](src/android/system/virtualmachine/VirtualMachineCallback.java). These
+are called when the following events happen:
+- `onPayloadStarted()`: The VM payload is about to be run.
+- `onPayloadReady()`: The VM payload is running and ready to accept
+ connections. (This notification is triggered by the payload code, using the
+ [`AVmPayload_notifyPayloadReady()`](../vm_payload/include/vm_payload.h)
+ function.)
+- `onPayloadFinished()`: The VM payload has exited normally. The exit code of
+ the VM (the value returned by [`AVmPayload_main()`](../vm_payload/README.md))
+ is supplied as a parameter.
+- `onError()`: The VM failed; something went wrong. An error code and
+ human-readable message are provided which may help diagnosing the problem.
+- `onStopped()`: The VM is no longer running. This is the final notification
+ from any VM run, whether or not it was successful. You can run the VM again
+ when you want to. A reason code indicating why the VM stopped is supplied as a
+ parameter.
+
+You can also query the status of a VM at any point by calling `getStatus()` on
+the `VirtualMachine` object. This will return one of the following values:
+- `STATUS_STOPPED`: The VM is not running - either it has not yet been started,
+ or it stopped after running.
+- `STATUS_RUNNING`: The VM is running. Your payload inside the VM may not be
+ running, since the VM may be in the process of starting or stopping.
+- `STATUS_DELETED`: The VM has been deleted, e.g. by calling the `delete()`
+ method on
+ [`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java). This
+ is irreversible - once a VM is in this state it will never leave it.
+
+Some methods on
+[`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java) can
+only be called when the VM status is `STATUS_RUNNING` (e.g. `stop()`), and some
+can only be called when the it is `STATUS_STOPPED` (e.g. `run()`).
+
+## VM Identity and Secrets
+
+Every VM has a 32-byte secret unique to it, which is not available to the
+host. We refer to this as the VM identity. The secret, and thus the identity,
+doesn’t normally change if the same VM is stopped and started, even after a
+reboot.
+
+In Android 14 the secret is derived, using the [Open Profile for
+DICE](https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/android.md),
+from:
+- A device-specific randomly generated value;
+- The complete system image;
+- A per-instance salt;
+- The code running in the VM, including the bootloader, kernel, Microdroid and
+ payload;
+- Significant VM configuration options, e.g. whether the VM is debuggable.
+
+Any change to any of these will mean a different secret is generated. So while
+an attacker could start a similar VM with maliciously altered code, that VM will
+not have access to the same secret. An attempt to start an existing VM instance
+which doesn't derive the same secret will fail.
+
+However, this also means that if the payload code changes - for example, your
+app is updated - then this also changes the identity. An existing VM instance
+will no longer be runnable, and you will have to delete it and create a new
+instance with a new secret.
+
+The payload code is not given direct access to the VM secret, but an API is
+provided to allow deterministically deriving further secrets from it,
+e.g. encryption or signing keys. See
+[`AVmPayload_getVmInstanceSecret()`](../vm_payload/include/vm_payload.h).
+
+Some VM configuration changes are allowed that don’t affect the identity -
+e.g. changing the number of CPUs or the amount of memory allocated to the
+VM. This can be done using the `setConfig()` method on
+[`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java).
+
+Deleting a VM (using the `delete()` method on
+[`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java))
+and recreating it will generate a new salt, so the new VM will have a different
+secret, even if it is otherwise identical.
+
+## Communicating with a VM
+
+Once the VM payload has successfully started you will probably want to establish
+communication between it and your app.
+
+Only the app that started a VM can connect to it. The VM can accept connections
+from the app, but cannot initiate connections to other VMs or other processes in
+the host Android.
+
+### Vsock
+
+The simplest form of communication is using a socket running over the
+[vsock](https://man7.org/linux/man-pages/man7/vsock.7.html) protocol.
+
+We suggest that the VM payload should create a listening socket (using the
+standard socket API) and then trigger the `onPayloadReady()` callback; the app
+can then connect to the socket. This helps to avoid a race condition where the
+app tries to connect before the VM is listening, necessitating a retry
+mechanism.
+
+In the payload this might look like this:
+
+```C++
+#include "vm_payload.h"
+
+extern "C" int AVmPayload_main() {
+ int fd = socket(AF_VSOCK, SOCK_STREAM, 0);
+ // bind, listen
+ AVmPayload_notifyPayloadReady();
+ // accept, read/write, ...
+}
+```
+
+And, in the app, like this:
+
+```Java
+void onPayloadReady(VirtualMachine vm) {
+ ParcelFileDescriptor pfd = vm.connectVsock(port);
+ // ...
+}
+```
+
+Vsock is useful for simple communication, or transferring of bulk data. For a
+richer RPC style of communication we suggest using Binder.
+
+### Binder
+
+The use of AIDL interfaces between the VM and app is supported via Binder RPC,
+which transmits messages over an underlying vsock socket.
+
+Note that Binder RPC has some limitations compared to the kernel Binder used in
+Android - for example file descriptors can't be sent. It also isn't possible to
+send a kernel Binder interface over Binder RPC, or vice versa.
+
+There is a payload API to allow an AIDL interface to be served over a specific
+vsock port, and the VirtualMachine class provides a way to connect to the VM and
+retrieve an instance of the interface.
+
+The payload code to serve a hypothetical `IPayload` interface might look like
+this:
+
+```C++
+class PayloadImpl : public BnPayload { ... };
+
+
+extern "C" int AVmPayload_main() {
+ auto service = ndk::SharedRefBase::make<PayloadImpl>();
+ auto callback = [](void*) {
+ AVmPayload_notifyPayloadReady();
+ };
+ AVmPayload_runVsockRpcServer(service->asBinder().get(),
+ port, callback, nullptr);
+}
+
+```
+
+And then the app code to connect to it could look like this:
+
+```Java
+void onPayloadReady(VirtualMachine vm) {
+ IPayload payload =
+ Payload.Stub.asInterface(vm.connectToVsockServer(port));
+ // ...
+}
+```
+
+## Stopping a VM
+
+You can stop a VM abruptly by calling the `stop()` method on the
+[`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java)
+instance. This is equivalent to turning off the power; the VM gets no
+opportunity to clean up at all. Any unwritten data might be lost.
+
+A better strategy might be to wait for the VM to exit cleanly (e.g. waiting for
+the `onStopped()` callback).
+
+Then you can arrange for your VM payload code to exit when it has finished its
+task (by returning from [`AVmPayload_main()`](../vm_payload/README.md), or
+calling `exit()`). Alternatively you could exit when you receive a request to do
+so from the app, e.g. via binder.
+
+When the VM payload does this you will receive an `onPayloadFinished()`
+callback, if you have installed a
+[`VirtualMachineCallback`](src/android/system/virtualmachine/VirtualMachineCallback.java),
+which includes the payload's exit code.
+
+Use of `stop()` should be reserved as a recovery mechanism - for example if the
+VM has not stopped within a reasonable time (a few seconds, say) after being
+requested to.
+
+The status of a VM will be `STATUS_STOPPED` if your `onStopped()` callback is
+invoked, or after a successful call to `stop()`. Note that your `onStopped()`
+will be called on the VM even if it ended as a result of a call to `stop()`.
+
+# Encrypted Storage
+
+When configuring a VM you can specify that it should have access to an encrypted
+storage filesystem of up to a specified size, using the
+`setEncryptedStorageBytes()` method on a
+[`VirtualMachineConfig`](src/android/system/virtualmachine/VirtualMachineConfig.java)
+builder.
+
+Inside the VM this storage is mounted at a path that can be retrieved via the
+[`AVmPayload_getEncryptedStoragePath()`](../vm_payload/include/vm_payload.h)
+function. The VM can create sub-directories and read and write files here. Any
+data written is persisted and should be available next time the VM is run. (An
+automatic sync is done when the payload exits normally.)
+
+Outside the VM the storage is persisted as a file in the app’s private data
+directory. The data is encrypted using a key derived from the VM secret, which
+is not made available outside the VM.
+
+So an attacker should not be able to decrypt the data; however, a sufficiently
+powerful attacker could delete it, wholly or partially roll it back to an
+earlier version, or modify it, corrupting the data.
+
+# Transferring a VM
+
+It is possible to make a copy of a VM instance. This can be used to transfer a
+VM from one app to another, which can be useful in some circumstances.
+
+This should only be done while the VM is stopped. The first step is to call
+`toDescriptor()` on the
+[`VirtualMachine`](src/android/system/virtualmachine/VirtualMachine.java)
+instance, which returns a
+[`VirtualMachineDescriptor`](src/android/system/virtualmachine/VirtualMachineDescriptor.java)
+object. This object internally contains open file descriptors to the files that
+hold the VM's state (its instance data, configuration, and encrypted storage).
+
+A `VirtualMachineDescriptor` is
+[`Parcelable`](https://developer.android.com/reference/android/os/Parcelable),
+so it can be passed to another app via a Binder call. Any app with a
+`VirtualMachineDescriptor` can pass it, along with a new VM name, to the
+`importFromDescriptor()` method on
+[`VirtualMachineManager`](src/android/system/virtualmachine/VirtualMachineManager.java). This
+is equivalent to calling `create()` with the same name and configuration, except
+that the new VM is the same instance as the original, with the same VM secret,
+and has access to a copy of the original's encrypted storage.
+
+Once the transfer has been completed it would be reasonable to delete the
+original VM, using the `delete()` method on `VirtualMachineManager`.
+
+
+
+
+
diff --git a/java/framework/api/current.txt b/java/framework/api/current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/java/framework/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/java/framework/api/module-lib-current.txt b/java/framework/api/module-lib-current.txt
new file mode 100644
index 0000000..4d59764
--- /dev/null
+++ b/java/framework/api/module-lib-current.txt
@@ -0,0 +1,9 @@
+// Signature format: 2.0
+package android.system.virtualmachine {
+
+ public class VirtualizationFrameworkInitializer {
+ method public static void registerServiceWrappers();
+ }
+
+}
+
diff --git a/java/framework/api/module-lib-removed.txt b/java/framework/api/module-lib-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/java/framework/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/java/framework/api/removed.txt b/java/framework/api/removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/java/framework/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/java/framework/api/system-current.txt b/java/framework/api/system-current.txt
new file mode 100644
index 0000000..d9bafa1
--- /dev/null
+++ b/java/framework/api/system-current.txt
@@ -0,0 +1,110 @@
+// Signature format: 2.0
+package android.system.virtualmachine {
+
+ public class VirtualMachine implements java.lang.AutoCloseable {
+ method public void clearCallback();
+ method @WorkerThread public void close();
+ method @NonNull @WorkerThread public android.os.IBinder connectToVsockServer(@IntRange(from=android.system.virtualmachine.VirtualMachine.MIN_VSOCK_PORT, to=android.system.virtualmachine.VirtualMachine.MAX_VSOCK_PORT) long) throws android.system.virtualmachine.VirtualMachineException;
+ method @NonNull @WorkerThread public android.os.ParcelFileDescriptor connectVsock(@IntRange(from=android.system.virtualmachine.VirtualMachine.MIN_VSOCK_PORT, to=android.system.virtualmachine.VirtualMachine.MAX_VSOCK_PORT) long) throws android.system.virtualmachine.VirtualMachineException;
+ method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachineConfig getConfig();
+ method @NonNull @WorkerThread public java.io.InputStream getConsoleOutput() throws android.system.virtualmachine.VirtualMachineException;
+ method @NonNull @WorkerThread public java.io.InputStream getLogOutput() throws android.system.virtualmachine.VirtualMachineException;
+ method @NonNull public String getName();
+ method @WorkerThread public int getStatus();
+ method @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) @WorkerThread public void run() throws android.system.virtualmachine.VirtualMachineException;
+ method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.system.virtualmachine.VirtualMachineCallback);
+ method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachineConfig setConfig(@NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+ method @WorkerThread public void stop() throws android.system.virtualmachine.VirtualMachineException;
+ method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachineDescriptor toDescriptor() throws android.system.virtualmachine.VirtualMachineException;
+ field public static final String MANAGE_VIRTUAL_MACHINE_PERMISSION = "android.permission.MANAGE_VIRTUAL_MACHINE";
+ field public static final long MAX_VSOCK_PORT = 4294967295L; // 0xffffffffL
+ field public static final long MIN_VSOCK_PORT = 1024L; // 0x400L
+ field public static final int STATUS_DELETED = 2; // 0x2
+ field public static final int STATUS_RUNNING = 1; // 0x1
+ field public static final int STATUS_STOPPED = 0; // 0x0
+ field public static final String USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION = "android.permission.USE_CUSTOM_VIRTUAL_MACHINE";
+ }
+
+ public interface VirtualMachineCallback {
+ method public void onError(@NonNull android.system.virtualmachine.VirtualMachine, int, @NonNull String);
+ method public void onPayloadFinished(@NonNull android.system.virtualmachine.VirtualMachine, int);
+ method public void onPayloadReady(@NonNull android.system.virtualmachine.VirtualMachine);
+ method public void onPayloadStarted(@NonNull android.system.virtualmachine.VirtualMachine);
+ method public void onStopped(@NonNull android.system.virtualmachine.VirtualMachine, int);
+ field public static final int ERROR_PAYLOAD_CHANGED = 2; // 0x2
+ field public static final int ERROR_PAYLOAD_INVALID_CONFIG = 3; // 0x3
+ field public static final int ERROR_PAYLOAD_VERIFICATION_FAILED = 1; // 0x1
+ field public static final int ERROR_UNKNOWN = 0; // 0x0
+ field public static final int STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED = 10; // 0xa
+ field public static final int STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH = 9; // 0x9
+ field public static final int STOP_REASON_CRASH = 6; // 0x6
+ field public static final int STOP_REASON_HANGUP = 16; // 0x10
+ field public static final int STOP_REASON_INFRASTRUCTURE_ERROR = 0; // 0x0
+ field public static final int STOP_REASON_KILLED = 1; // 0x1
+ field public static final int STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE = 11; // 0xb
+ field public static final int STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG = 14; // 0xe
+ field public static final int STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED = 12; // 0xc
+ field public static final int STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED = 13; // 0xd
+ field public static final int STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR = 15; // 0xf
+ field public static final int STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED = 8; // 0x8
+ field public static final int STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH = 7; // 0x7
+ field public static final int STOP_REASON_REBOOT = 5; // 0x5
+ field public static final int STOP_REASON_SHUTDOWN = 3; // 0x3
+ field public static final int STOP_REASON_START_FAILED = 4; // 0x4
+ field public static final int STOP_REASON_UNKNOWN = 2; // 0x2
+ field public static final int STOP_REASON_VIRTUALIZATION_SERVICE_DIED = -1; // 0xffffffff
+ }
+
+ public final class VirtualMachineConfig {
+ method @Nullable public String getApkPath();
+ method public int getCpuTopology();
+ method public int getDebugLevel();
+ method @IntRange(from=0) public long getEncryptedStorageBytes();
+ method @IntRange(from=0) public long getMemoryBytes();
+ method @Nullable public String getPayloadBinaryName();
+ method public boolean isCompatibleWith(@NonNull android.system.virtualmachine.VirtualMachineConfig);
+ method public boolean isEncryptedStorageEnabled();
+ method public boolean isProtectedVm();
+ method public boolean isVmOutputCaptured();
+ field public static final int CPU_TOPOLOGY_MATCH_HOST = 1; // 0x1
+ field public static final int CPU_TOPOLOGY_ONE_CPU = 0; // 0x0
+ field public static final int DEBUG_LEVEL_FULL = 1; // 0x1
+ field public static final int DEBUG_LEVEL_NONE = 0; // 0x0
+ }
+
+ public static final class VirtualMachineConfig.Builder {
+ ctor public VirtualMachineConfig.Builder(@NonNull android.content.Context);
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig build();
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setApkPath(@NonNull String);
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setCpuTopology(int);
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setDebugLevel(int);
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setEncryptedStorageBytes(@IntRange(from=1) long);
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setMemoryBytes(@IntRange(from=1) long);
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadBinaryName(@NonNull String);
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setProtectedVm(boolean);
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setVmOutputCaptured(boolean);
+ }
+
+ public final class VirtualMachineDescriptor implements java.lang.AutoCloseable android.os.Parcelable {
+ method public void close();
+ method public int describeContents();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.system.virtualmachine.VirtualMachineDescriptor> CREATOR;
+ }
+
+ public class VirtualMachineException extends java.lang.Exception {
+ }
+
+ public class VirtualMachineManager {
+ method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) @WorkerThread public android.system.virtualmachine.VirtualMachine create(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+ method @WorkerThread public void delete(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
+ method @Nullable @WorkerThread public android.system.virtualmachine.VirtualMachine get(@NonNull String) throws android.system.virtualmachine.VirtualMachineException;
+ method public int getCapabilities();
+ method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachine getOrCreate(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineConfig) throws android.system.virtualmachine.VirtualMachineException;
+ method @NonNull @WorkerThread public android.system.virtualmachine.VirtualMachine importFromDescriptor(@NonNull String, @NonNull android.system.virtualmachine.VirtualMachineDescriptor) throws android.system.virtualmachine.VirtualMachineException;
+ field public static final int CAPABILITY_NON_PROTECTED_VM = 2; // 0x2
+ field public static final int CAPABILITY_PROTECTED_VM = 1; // 0x1
+ }
+
+}
+
diff --git a/java/framework/api/system-removed.txt b/java/framework/api/system-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/java/framework/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/java/framework/api/test-current.txt b/java/framework/api/test-current.txt
new file mode 100644
index 0000000..3cd8e42
--- /dev/null
+++ b/java/framework/api/test-current.txt
@@ -0,0 +1,36 @@
+// Signature format: 2.0
+package android.system.virtualmachine {
+
+ public class VirtualMachine implements java.lang.AutoCloseable {
+ method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public void enableTestAttestation() throws android.system.virtualmachine.VirtualMachineException;
+ method @NonNull @WorkerThread public java.io.OutputStream getConsoleInput() throws android.system.virtualmachine.VirtualMachineException;
+ method @NonNull public java.io.File getRootDir();
+ }
+
+ public final class VirtualMachineConfig {
+ method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public java.util.List<java.lang.String> getExtraApks();
+ method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @Nullable public String getOs();
+ method @Nullable public String getPayloadConfigPath();
+ method public boolean isVmConsoleInputSupported();
+ }
+
+ public static final class VirtualMachineConfig.Builder {
+ method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder addExtraApk(@NonNull String);
+ method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setOs(@NonNull String);
+ method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadConfigPath(@NonNull String);
+ method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setVendorDiskImage(@NonNull java.io.File);
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setVmConsoleInputSupported(boolean);
+ }
+
+ public class VirtualMachineManager {
+ method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public java.util.List<java.lang.String> getSupportedOSList() throws android.system.virtualmachine.VirtualMachineException;
+ method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @RequiresPermission(android.system.virtualmachine.VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION) public boolean isFeatureEnabled(String) throws android.system.virtualmachine.VirtualMachineException;
+ field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_DICE_CHANGES = "com.android.kvm.DICE_CHANGES";
+ field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_LLPVM_CHANGES = "com.android.kvm.LLPVM_CHANGES";
+ field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_MULTI_TENANT = "com.android.kvm.MULTI_TENANT";
+ field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_REMOTE_ATTESTATION = "com.android.kvm.REMOTE_ATTESTATION";
+ field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_VENDOR_MODULES = "com.android.kvm.VENDOR_MODULES";
+ }
+
+}
+
diff --git a/java/framework/api/test-removed.txt b/java/framework/api/test-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/java/framework/api/test-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/java/framework/jarjar-rules.txt b/java/framework/jarjar-rules.txt
new file mode 100644
index 0000000..726f9aa
--- /dev/null
+++ b/java/framework/jarjar-rules.txt
@@ -0,0 +1,10 @@
+# Rules for the android.system.virtualmachine java_sdk_library.
+
+# Keep the API surface, most of it is accessible from VirtualMachineManager
+keep android.system.virtualmachine.VirtualMachineManager
+# VirtualizationModuleFrameworkInitializer is not accessible from
+# VirtualMachineManager, we need to explicitly keep it.
+keep android.system.virtualmachine.VirtualizationFrameworkInitializer
+
+# We statically link PlatformProperties, rename to avoid clashes.
+rule android.sysprop.** com.android.system.virtualmachine.sysprop.@1
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
new file mode 100644
index 0000000..6b03cfe
--- /dev/null
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -0,0 +1,1505 @@
+/*
+ * 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.AutoCloseInputStream;
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_CHANGED;
+import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_INVALID_CONFIG;
+import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_VERIFICATION_FAILED;
+import static android.system.virtualmachine.VirtualMachineCallback.ERROR_UNKNOWN;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_CRASH;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_HANGUP;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_INFRASTRUCTURE_ERROR;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_KILLED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_REBOOT;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_SHUTDOWN;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_START_FAILED;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_UNKNOWN;
+import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_VIRTUALIZATION_SERVICE_DIED;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.WorkerThread;
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.virtualizationcommon.DeathReason;
+import android.system.virtualizationcommon.ErrorCode;
+import android.system.virtualizationservice.IVirtualMachine;
+import android.system.virtualizationservice.IVirtualMachineCallback;
+import android.system.virtualizationservice.IVirtualizationService;
+import android.system.virtualizationservice.MemoryTrimLevel;
+import android.system.virtualizationservice.PartitionType;
+import android.system.virtualizationservice.VirtualMachineAppConfig;
+import android.system.virtualizationservice.VirtualMachineState;
+import android.util.JsonReader;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.system.virtualmachine.flags.Flags;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.channels.FileChannel;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.zip.ZipFile;
+
+/**
+ * Represents an VM instance, with its own configuration and state. Instances are persistent and are
+ * created or retrieved via {@link VirtualMachineManager}.
+ *
+ * <p>The {@link #run} method actually starts up the VM and allows the payload code to execute. It
+ * will continue until it exits or {@link #stop} is called. Updates on the state of the VM can be
+ * received using {@link #setCallback}. The app can communicate with the VM using {@link
+ * #connectToVsockServer} or {@link #connectVsock}.
+ *
+ * <p>The payload code running inside the VM has access to a set of native APIs; see the <a
+ * href="https://cs.android.com/android/platform/superproject/+/master:packages/modules/Virtualization/vm_payload/README.md">README
+ * file</a> for details.
+ *
+ * <p>Each VM has a unique secret, computed from the APK that contains the code running in it, the
+ * VM configuration, and a random per-instance salt. The secret can be accessed by the payload code
+ * running inside the VM (using {@code AVmPayload_getVmInstanceSecret}) but is not made available
+ * outside it.
+ *
+ * @hide
+ */
+@SystemApi
+public class VirtualMachine implements AutoCloseable {
+ /** The permission needed to create or run a virtual machine. */
+ public static final String MANAGE_VIRTUAL_MACHINE_PERMISSION =
+ "android.permission.MANAGE_VIRTUAL_MACHINE";
+
+ /**
+ * The permission needed to create a virtual machine with more advanced configuration options.
+ */
+ public static final String USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION =
+ "android.permission.USE_CUSTOM_VIRTUAL_MACHINE";
+
+ /**
+ * The lowest port number that can be used to communicate with the virtual machine payload.
+ *
+ * @see #connectToVsockServer
+ * @see #connectVsock
+ */
+ @SuppressLint("MinMaxConstant") // Won't change: see man 7 vsock.
+ public static final long MIN_VSOCK_PORT = 1024;
+
+ /**
+ * The highest port number that can be used to communicate with the virtual machine payload.
+ *
+ * @see #connectToVsockServer
+ * @see #connectVsock
+ */
+ @SuppressLint("MinMaxConstant") // Won't change: see man 7 vsock.
+ public static final long MAX_VSOCK_PORT = (1L << 32) - 1;
+
+ /**
+ * Status of a virtual machine
+ *
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "STATUS_", value = {
+ STATUS_STOPPED,
+ STATUS_RUNNING,
+ STATUS_DELETED
+ })
+ public @interface Status {}
+
+ /** The virtual machine has just been created, or {@link #stop} was called on it. */
+ public static final int STATUS_STOPPED = 0;
+
+ /** The virtual machine is running. */
+ public static final int STATUS_RUNNING = 1;
+
+ /**
+ * The virtual machine has been deleted. This is an irreversible state. Once a virtual machine
+ * is deleted all its secrets are permanently lost, and it cannot be run. A new virtual machine
+ * with the same name and config may be created, with new and different secrets.
+ */
+ public static final int STATUS_DELETED = 2;
+
+ private static final String TAG = "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 idsig file for a VM */
+ private static final String IDSIG_FILE = "idsig";
+
+ /** Name of the idsig files for extra APKs. */
+ private static final String EXTRA_IDSIG_FILE_PREFIX = "extra_idsig_";
+
+ /** Size of the instance image. 10 MB. */
+ private static final long INSTANCE_FILE_SIZE = 10 * 1024 * 1024;
+
+ /** Name of the file backing the encrypted storage */
+ private static final String ENCRYPTED_STORE_FILE = "storage.img";
+
+ /** The package which owns this VM. */
+ @NonNull private final String mPackageName;
+
+ /** Name of this VM within the package. The name should be unique in the package. */
+ @NonNull private final String mName;
+
+ /**
+ * Path to the directory containing all the files related to this VM.
+ */
+ @NonNull private final File mVmRootPath;
+
+ /**
+ * Path to the config file for this VM. The config file is where the configuration is persisted.
+ */
+ @NonNull private final File mConfigFilePath;
+
+ /** Path to the instance image file for this VM. */
+ @NonNull private final File mInstanceFilePath;
+
+ /** Path to the idsig file for this VM. */
+ @NonNull private final File mIdsigFilePath;
+
+ /** File that backs the encrypted storage - Will be null if not enabled. */
+ @Nullable private final File mEncryptedStoreFilePath;
+
+ /**
+ * Unmodifiable list of extra apks. Apks are specified by the vm config, and corresponding
+ * idsigs are to be generated.
+ */
+ @NonNull private final List<ExtraApkSpec> mExtraApks;
+
+ private class MemoryManagementCallbacks implements ComponentCallbacks2 {
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {}
+
+ @Override
+ public void onLowMemory() {}
+
+ @Override
+ public void onTrimMemory(int level) {
+ @MemoryTrimLevel int vmTrimLevel;
+
+ switch (level) {
+ case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
+ vmTrimLevel = MemoryTrimLevel.TRIM_MEMORY_RUNNING_CRITICAL;
+ break;
+ case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
+ vmTrimLevel = MemoryTrimLevel.TRIM_MEMORY_RUNNING_LOW;
+ break;
+ case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
+ vmTrimLevel = MemoryTrimLevel.TRIM_MEMORY_RUNNING_MODERATE;
+ break;
+ case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
+ case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
+ case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
+ /* Release as much memory as we can. The app is on the LMKD LRU kill list. */
+ vmTrimLevel = MemoryTrimLevel.TRIM_MEMORY_RUNNING_CRITICAL;
+ break;
+ default:
+ /* Treat unrecognised messages as generic low-memory warnings. */
+ vmTrimLevel = MemoryTrimLevel.TRIM_MEMORY_RUNNING_LOW;
+ break;
+ }
+
+ synchronized (mLock) {
+ try {
+ if (mVirtualMachine != null) {
+ mVirtualMachine.onTrimMemory(vmTrimLevel);
+ }
+ } catch (Exception e) {
+ /* Caller doesn't want our exceptions. Log them instead. */
+ Log.w(TAG, "TrimMemory failed: ", e);
+ }
+ }
+ }
+ }
+
+ /** Running instance of virtmgr that hosts VirtualizationService for this VM. */
+ @NonNull private final VirtualizationService mVirtualizationService;
+
+ @NonNull private final MemoryManagementCallbacks mMemoryManagementCallbacks;
+
+ @NonNull private final Context mContext;
+
+ // A note on lock ordering:
+ // You can take mLock while holding VirtualMachineManager.sCreateLock, but not vice versa.
+ // We never take any other lock while holding mCallbackLock; therefore you can
+ // take mCallbackLock while holding any other lock.
+
+ /** Lock protecting our mutable state (other than callbacks). */
+ private final Object mLock = new Object();
+
+ /** Lock protecting callbacks. */
+ private final Object mCallbackLock = new Object();
+
+ private final boolean mVmOutputCaptured;
+
+ private final boolean mVmConsoleInputSupported;
+
+ /** The configuration that is currently associated with this VM. */
+ @GuardedBy("mLock")
+ @NonNull
+ private VirtualMachineConfig mConfig;
+
+ /** Handle to the "running" VM. */
+ @GuardedBy("mLock")
+ @Nullable
+ private IVirtualMachine mVirtualMachine;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ParcelFileDescriptor mConsoleOutReader;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ParcelFileDescriptor mConsoleOutWriter;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ParcelFileDescriptor mConsoleInReader;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ParcelFileDescriptor mConsoleInWriter;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ParcelFileDescriptor mLogReader;
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ParcelFileDescriptor mLogWriter;
+
+ @GuardedBy("mLock")
+ private boolean mWasDeleted = false;
+
+ /** The registered callback */
+ @GuardedBy("mCallbackLock")
+ @Nullable
+ private VirtualMachineCallback mCallback;
+
+ /** The executor on which the callback will be executed */
+ @GuardedBy("mCallbackLock")
+ @Nullable
+ private Executor mCallbackExecutor;
+
+ private static class ExtraApkSpec {
+ public final File apk;
+ public final File idsig;
+
+ ExtraApkSpec(File apk, File idsig) {
+ this.apk = apk;
+ this.idsig = idsig;
+ }
+ }
+
+ static {
+ System.loadLibrary("virtualmachine_jni");
+ }
+
+ private VirtualMachine(
+ @NonNull Context context,
+ @NonNull String name,
+ @NonNull VirtualMachineConfig config,
+ @NonNull VirtualizationService service)
+ throws VirtualMachineException {
+ mPackageName = context.getPackageName();
+ mName = requireNonNull(name, "Name must not be null");
+ mConfig = requireNonNull(config, "Config must not be null");
+ mVirtualizationService = service;
+
+ File thisVmDir = getVmDir(context, mName);
+ mVmRootPath = thisVmDir;
+ mConfigFilePath = new File(thisVmDir, CONFIG_FILE);
+ mInstanceFilePath = new File(thisVmDir, INSTANCE_IMAGE_FILE);
+ mIdsigFilePath = new File(thisVmDir, IDSIG_FILE);
+ mExtraApks = setupExtraApks(context, config, thisVmDir);
+ mMemoryManagementCallbacks = new MemoryManagementCallbacks();
+ mContext = context;
+ mEncryptedStoreFilePath =
+ (config.isEncryptedStorageEnabled())
+ ? new File(thisVmDir, ENCRYPTED_STORE_FILE)
+ : null;
+
+ mVmOutputCaptured = config.isVmOutputCaptured();
+ mVmConsoleInputSupported = config.isVmConsoleInputSupported();
+ }
+
+ /**
+ * Creates a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
+ * with the given name.
+ *
+ * <p>The new virtual machine will be in the same state as the descriptor indicates.
+ *
+ * <p>Once a virtual machine is imported it is persisted until it is deleted by calling {@link
+ * #delete}. The imported virtual machine is in {@link #STATUS_STOPPED} state. To run the VM,
+ * call {@link #run}.
+ */
+ @GuardedBy("VirtualMachineManager.sCreateLock")
+ @NonNull
+ static VirtualMachine fromDescriptor(
+ @NonNull Context context,
+ @NonNull String name,
+ @NonNull VirtualMachineDescriptor vmDescriptor)
+ throws VirtualMachineException {
+ File vmDir = createVmDir(context, name);
+ try {
+ VirtualMachine vm;
+ try (vmDescriptor) {
+ VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
+ vm = new VirtualMachine(context, name, config, VirtualizationService.getInstance());
+ config.serialize(vm.mConfigFilePath);
+ try {
+ vm.mInstanceFilePath.createNewFile();
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to create instance image", e);
+ }
+ vm.importInstanceFrom(vmDescriptor.getInstanceImgFd());
+
+ if (vmDescriptor.getEncryptedStoreFd() != null) {
+ try {
+ vm.mEncryptedStoreFilePath.createNewFile();
+ } catch (IOException e) {
+ throw new VirtualMachineException(
+ "failed to create encrypted storage image", e);
+ }
+ vm.importEncryptedStoreFrom(vmDescriptor.getEncryptedStoreFd());
+ }
+ }
+ return vm;
+ } catch (VirtualMachineException | RuntimeException e) {
+ // If anything goes wrong, delete any files created so far and the VM's directory
+ try {
+ deleteRecursively(vmDir);
+ } catch (IOException innerException) {
+ e.addSuppressed(innerException);
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * 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 #STATUS_STOPPED} state. To run the VM, call {@link #run}.
+ */
+ @GuardedBy("VirtualMachineManager.sCreateLock")
+ @NonNull
+ static VirtualMachine create(
+ @NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config)
+ throws VirtualMachineException {
+ File vmDir = createVmDir(context, name);
+
+ try {
+ VirtualMachine vm =
+ new VirtualMachine(context, name, config, VirtualizationService.getInstance());
+ config.serialize(vm.mConfigFilePath);
+ try {
+ vm.mInstanceFilePath.createNewFile();
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to create instance image", e);
+ }
+ if (config.isEncryptedStorageEnabled()) {
+ try {
+ vm.mEncryptedStoreFilePath.createNewFile();
+ } catch (IOException e) {
+ throw new VirtualMachineException(
+ "failed to create encrypted storage image", e);
+ }
+ }
+
+ IVirtualizationService service = vm.mVirtualizationService.getBinder();
+
+ try {
+ service.initializeWritablePartition(
+ ParcelFileDescriptor.open(vm.mInstanceFilePath, MODE_READ_WRITE),
+ INSTANCE_FILE_SIZE,
+ PartitionType.ANDROID_VM_INSTANCE);
+ } catch (FileNotFoundException e) {
+ throw new VirtualMachineException("instance image missing", e);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ } catch (ServiceSpecificException | IllegalArgumentException e) {
+ throw new VirtualMachineException("failed to create instance partition", e);
+ }
+
+ if (config.isEncryptedStorageEnabled()) {
+ try {
+ service.initializeWritablePartition(
+ ParcelFileDescriptor.open(vm.mEncryptedStoreFilePath, MODE_READ_WRITE),
+ config.getEncryptedStorageBytes(),
+ PartitionType.ENCRYPTEDSTORE);
+ } catch (FileNotFoundException e) {
+ throw new VirtualMachineException("encrypted storage image missing", e);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ } catch (ServiceSpecificException | IllegalArgumentException e) {
+ throw new VirtualMachineException(
+ "failed to create encrypted storage partition", e);
+ }
+ }
+ return vm;
+ } catch (VirtualMachineException | RuntimeException e) {
+ // If anything goes wrong, delete any files created so far and the VM's directory
+ try {
+ deleteRecursively(vmDir);
+ } catch (IOException innerException) {
+ e.addSuppressed(innerException);
+ }
+ throw e;
+ }
+ }
+
+ /** Loads a virtual machine that is already created before. */
+ @GuardedBy("VirtualMachineManager.sCreateLock")
+ @Nullable
+ static VirtualMachine load(@NonNull Context context, @NonNull String name)
+ throws VirtualMachineException {
+ File thisVmDir = getVmDir(context, name);
+ if (!thisVmDir.exists()) {
+ // The VM doesn't exist.
+ return null;
+ }
+ File configFilePath = new File(thisVmDir, CONFIG_FILE);
+ VirtualMachineConfig config = VirtualMachineConfig.from(configFilePath);
+ VirtualMachine vm =
+ new VirtualMachine(context, name, config, VirtualizationService.getInstance());
+
+ if (!vm.mInstanceFilePath.exists()) {
+ throw new VirtualMachineException("instance image missing");
+ }
+ if (config.isEncryptedStorageEnabled() && !vm.mEncryptedStoreFilePath.exists()) {
+ throw new VirtualMachineException("Storage image missing");
+ }
+ return vm;
+ }
+
+ @GuardedBy("VirtualMachineManager.sCreateLock")
+ void delete(Context context, String name) throws VirtualMachineException {
+ synchronized (mLock) {
+ checkStopped();
+ // Once we explicitly delete a VM it must remain permanently in the deleted state;
+ // if a new VM is created with the same name (and files) that's unrelated.
+ mWasDeleted = true;
+ }
+ deleteVmDirectory(context, name);
+ }
+
+ static void deleteVmDirectory(Context context, String name) throws VirtualMachineException {
+ try {
+ deleteRecursively(getVmDir(context, name));
+ } catch (IOException e) {
+ throw new VirtualMachineException(e);
+ }
+ }
+
+ @GuardedBy("VirtualMachineManager.sCreateLock")
+ @NonNull
+ private static File createVmDir(@NonNull Context context, @NonNull String name)
+ throws VirtualMachineException {
+ File vmDir = getVmDir(context, name);
+ try {
+ // We don't need to undo this even if VM creation fails.
+ Files.createDirectories(vmDir.getParentFile().toPath());
+
+ // The checking of the existence of this directory and the creation of it is done
+ // atomically. If the directory already exists (i.e. the VM with the same name was
+ // already created), FileAlreadyExistsException is thrown.
+ Files.createDirectory(vmDir.toPath());
+ } catch (FileAlreadyExistsException e) {
+ throw new VirtualMachineException("virtual machine already exists", e);
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to create directory for VM", e);
+ }
+ return vmDir;
+ }
+
+ @NonNull
+ private static File getVmDir(@NonNull Context context, @NonNull String name) {
+ if (name.contains(File.separator) || name.equals(".") || name.equals("..")) {
+ throw new IllegalArgumentException("Invalid VM name: " + name);
+ }
+ File vmRoot = new File(context.getDataDir(), VM_DIR);
+ return new File(vmRoot, name);
+ }
+
+ /**
+ * Returns the name of this virtual machine. The name is unique in the package and can't be
+ * changed.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ 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; they have different secrets. It is also possible that a virtual
+ * machine can change its config, which can be done by calling {@link #setConfig}.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ @NonNull
+ public VirtualMachineConfig getConfig() {
+ synchronized (mLock) {
+ return mConfig;
+ }
+ }
+
+ /**
+ * Returns the current status of this virtual machine.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ @Status
+ public int getStatus() {
+ IVirtualMachine virtualMachine;
+ synchronized (mLock) {
+ if (mWasDeleted) {
+ return STATUS_DELETED;
+ }
+ virtualMachine = mVirtualMachine;
+ }
+
+ int status;
+ if (virtualMachine == null) {
+ status = STATUS_STOPPED;
+ } else {
+ try {
+ status = stateToStatus(virtualMachine.getState());
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+ if (status == STATUS_STOPPED && !mVmRootPath.exists()) {
+ // A VM can quite happily keep running if its backing files have been deleted.
+ // But once it stops, it's gone forever.
+ synchronized (mLock) {
+ dropVm();
+ }
+ return STATUS_DELETED;
+ }
+ return status;
+ }
+
+ private int stateToStatus(@VirtualMachineState int state) {
+ switch (state) {
+ case VirtualMachineState.STARTING:
+ case VirtualMachineState.STARTED:
+ case VirtualMachineState.READY:
+ case VirtualMachineState.FINISHED:
+ return STATUS_RUNNING;
+ case VirtualMachineState.NOT_STARTED:
+ case VirtualMachineState.DEAD:
+ default:
+ return STATUS_STOPPED;
+ }
+ }
+
+ // Throw an appropriate exception if we have a running VM, or the VM has been deleted.
+ @GuardedBy("mLock")
+ private void checkStopped() throws VirtualMachineException {
+ if (mWasDeleted || !mVmRootPath.exists()) {
+ throw new VirtualMachineException("VM has been deleted");
+ }
+ if (mVirtualMachine == null) {
+ return;
+ }
+ try {
+ if (stateToStatus(mVirtualMachine.getState()) != STATUS_STOPPED) {
+ throw new VirtualMachineException("VM is not in stopped state");
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ // It's stopped, but we still have a reference to it - we can fix that.
+ dropVm();
+ }
+
+ /**
+ * This should only be called when we know our VM has stopped; we no longer need to hold a
+ * reference to it (this allows resources to be GC'd) and we no longer need to be informed of
+ * memory pressure.
+ */
+ @GuardedBy("mLock")
+ private void dropVm() {
+ mContext.unregisterComponentCallbacks(mMemoryManagementCallbacks);
+ mVirtualMachine = null;
+ }
+
+ /** If we have an IVirtualMachine in the running state return it, otherwise throw. */
+ @GuardedBy("mLock")
+ private IVirtualMachine getRunningVm() throws VirtualMachineException {
+ try {
+ if (mVirtualMachine != null
+ && stateToStatus(mVirtualMachine.getState()) == STATUS_RUNNING) {
+ return mVirtualMachine;
+ } else {
+ if (mWasDeleted || !mVmRootPath.exists()) {
+ throw new VirtualMachineException("VM has been deleted");
+ } else {
+ throw new VirtualMachineException("VM is not in running state");
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
+ * Registers the callback object to get events from the virtual machine. If a callback was
+ * already registered, it is replaced with the new one.
+ *
+ * @hide
+ */
+ @SystemApi
+ public void setCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull VirtualMachineCallback callback) {
+ synchronized (mCallbackLock) {
+ mCallback = callback;
+ mCallbackExecutor = executor;
+ }
+ }
+
+ /**
+ * Clears the currently registered callback.
+ *
+ * @hide
+ */
+ @SystemApi
+ public void clearCallback() {
+ synchronized (mCallbackLock) {
+ mCallback = null;
+ mCallbackExecutor = null;
+ }
+ }
+
+ /** Executes a callback on the callback executor. */
+ private void executeCallback(Consumer<VirtualMachineCallback> fn) {
+ final VirtualMachineCallback callback;
+ final Executor executor;
+ synchronized (mCallbackLock) {
+ callback = mCallback;
+ executor = mCallbackExecutor;
+ }
+ if (callback == null || executor == null) {
+ return;
+ }
+ final long restoreToken = Binder.clearCallingIdentity();
+ try {
+ executor.execute(() -> fn.accept(callback));
+ } finally {
+ Binder.restoreCallingIdentity(restoreToken);
+ }
+ }
+
+ /**
+ * 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 using {@link #setCallback} before calling {@code run()}.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @throws VirtualMachineException if the virtual machine is not stopped or could not be
+ * started.
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ @RequiresPermission(MANAGE_VIRTUAL_MACHINE_PERMISSION)
+ public void run() throws VirtualMachineException {
+ synchronized (mLock) {
+ checkStopped();
+
+ try {
+ mIdsigFilePath.createNewFile();
+ for (ExtraApkSpec extraApk : mExtraApks) {
+ extraApk.idsig.createNewFile();
+ }
+ } catch (IOException e) {
+ // If the file already exists, exception is not thrown.
+ throw new VirtualMachineException("Failed to create APK signature file", e);
+ }
+
+ IVirtualizationService service = mVirtualizationService.getBinder();
+
+ try {
+ if (mVmOutputCaptured) {
+ createVmOutputPipes();
+ }
+
+ if (mVmConsoleInputSupported) {
+ createVmInputPipes();
+ }
+
+ VirtualMachineConfig vmConfig = getConfig();
+ VirtualMachineAppConfig appConfig =
+ vmConfig.toVsConfig(mContext.getPackageManager());
+ appConfig.name = mName;
+
+ if (!vmConfig.getExtraApks().isEmpty()) {
+ // Extra APKs were specified directly, rather than via config file.
+ // We've already populated the file names for the extra APKs and IDSigs
+ // (via setupExtraApks). But we also need to open the APK files and add
+ // fds for them to the payload config.
+ // This isn't needed when the extra APKs are specified in a config file - then
+ // Virtualization Manager opens them itself.
+ List<ParcelFileDescriptor> extraApkFiles = new ArrayList<>(mExtraApks.size());
+ for (ExtraApkSpec extraApk : mExtraApks) {
+ try {
+ extraApkFiles.add(
+ ParcelFileDescriptor.open(extraApk.apk, MODE_READ_ONLY));
+ } catch (FileNotFoundException e) {
+ throw new VirtualMachineException("Failed to open extra APK", e);
+ }
+ }
+ appConfig.payload.getPayloadConfig().extraApks = extraApkFiles;
+ }
+
+ try {
+ createIdSigs(service, appConfig);
+ } catch (FileNotFoundException e) {
+ throw new VirtualMachineException("Failed to generate APK signature", e);
+ }
+
+ android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
+ android.system.virtualizationservice.VirtualMachineConfig.appConfig(
+ appConfig);
+
+ mVirtualMachine =
+ service.createVm(
+ vmConfigParcel, mConsoleOutWriter, mConsoleInReader, mLogWriter);
+ mVirtualMachine.registerCallback(new CallbackTranslator(service));
+ mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
+ mVirtualMachine.start();
+ } catch (IllegalStateException | ServiceSpecificException e) {
+ throw new VirtualMachineException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+ }
+
+ private void createIdSigs(IVirtualizationService service, VirtualMachineAppConfig appConfig)
+ throws RemoteException, FileNotFoundException {
+ // Fill the idsig file by hashing the apk
+ service.createOrUpdateIdsigFile(
+ appConfig.apk, ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_WRITE));
+
+ for (ExtraApkSpec extraApk : mExtraApks) {
+ service.createOrUpdateIdsigFile(
+ ParcelFileDescriptor.open(extraApk.apk, MODE_READ_ONLY),
+ ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_WRITE));
+ }
+
+ // Re-open idsig files in read-only mode
+ appConfig.idsig = ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_ONLY);
+ appConfig.instanceImage = ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
+ if (mEncryptedStoreFilePath != null) {
+ appConfig.encryptedStorageImage =
+ ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_WRITE);
+ }
+ List<ParcelFileDescriptor> extraIdsigs = new ArrayList<>();
+ for (ExtraApkSpec extraApk : mExtraApks) {
+ extraIdsigs.add(ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_ONLY));
+ }
+ appConfig.extraIdsigs = extraIdsigs;
+ }
+
+ @GuardedBy("mLock")
+ private void createVmOutputPipes() throws VirtualMachineException {
+ try {
+ if (mConsoleOutReader == null || mConsoleOutWriter == null) {
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ mConsoleOutReader = pipe[0];
+ mConsoleOutWriter = pipe[1];
+ }
+
+ if (mLogReader == null || mLogWriter == null) {
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ mLogReader = pipe[0];
+ mLogWriter = pipe[1];
+ }
+ } catch (IOException e) {
+ throw new VirtualMachineException("Failed to create output stream for VM", e);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void createVmInputPipes() throws VirtualMachineException {
+ try {
+ if (mConsoleInReader == null || mConsoleInWriter == null) {
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ mConsoleInReader = pipe[0];
+ mConsoleInWriter = pipe[1];
+ }
+ } catch (IOException e) {
+ throw new VirtualMachineException("Failed to create input stream for VM", e);
+ }
+ }
+
+ /**
+ * Returns the stream object representing the console output from the virtual machine. The
+ * console output is only available if the {@link VirtualMachineConfig} specifies that it should
+ * be {@linkplain VirtualMachineConfig#isVmOutputCaptured captured}.
+ *
+ * <p>If you turn on output capture, you must consume data from {@code getConsoleOutput} -
+ * because otherwise the code in the VM may get blocked when the pipe buffer fills up.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @throws VirtualMachineException if the stream could not be created, or capturing is turned
+ * off.
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ @NonNull
+ public InputStream getConsoleOutput() throws VirtualMachineException {
+ if (!mVmOutputCaptured) {
+ throw new VirtualMachineException("Capturing vm outputs is turned off");
+ }
+ synchronized (mLock) {
+ createVmOutputPipes();
+ return new FileInputStream(mConsoleOutReader.getFileDescriptor());
+ }
+ }
+
+ /**
+ * Returns the stream object representing the console input to the virtual machine. The console
+ * input is only available if the {@link VirtualMachineConfig} specifies that it should be
+ * {@linkplain VirtualMachineConfig#isVmConsoleInputSupported supported}.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @throws VirtualMachineException if the stream could not be created, or console input is not
+ * supported.
+ * @hide
+ */
+ @TestApi
+ @WorkerThread
+ @NonNull
+ public OutputStream getConsoleInput() throws VirtualMachineException {
+ if (!mVmConsoleInputSupported) {
+ throw new VirtualMachineException("VM console input is not supported");
+ }
+ synchronized (mLock) {
+ createVmInputPipes();
+ return new FileOutputStream(mConsoleInWriter.getFileDescriptor());
+ }
+ }
+
+
+ /**
+ * Returns the stream object representing the log output from the virtual machine. The log
+ * output is only available if the VirtualMachineConfig specifies that it should be {@linkplain
+ * VirtualMachineConfig#isVmOutputCaptured captured}.
+ *
+ * <p>If you turn on output capture, you must consume data from {@code getLogOutput} - because
+ * otherwise the code in the VM may get blocked when the pipe buffer fills up.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @throws VirtualMachineException if the stream could not be created, or capturing is turned
+ * off.
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ @NonNull
+ public InputStream getLogOutput() throws VirtualMachineException {
+ if (!mVmOutputCaptured) {
+ throw new VirtualMachineException("Capturing vm outputs is turned off");
+ }
+ synchronized (mLock) {
+ createVmOutputPipes();
+ return new FileInputStream(mLogReader.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 of the event. Writes to {@linkplain
+ * VirtualMachineConfig.Builder#setEncryptedStorageBytes encrypted storage} might not be
+ * persisted, and the instance might be left in an inconsistent state.
+ *
+ * <p>For a graceful shutdown, you could request the payload to call {@code exit()}, e.g. via a
+ * {@linkplain #connectToVsockServer binder request}, and wait for {@link
+ * VirtualMachineCallback#onPayloadFinished} to be called.
+ *
+ * <p>A stopped virtual machine can be re-started by calling {@link #run()}.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @throws VirtualMachineException if the virtual machine is not running or could not be
+ * stopped.
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ public void stop() throws VirtualMachineException {
+ synchronized (mLock) {
+ if (mVirtualMachine == null) {
+ throw new VirtualMachineException("VM is not running");
+ }
+ try {
+ mVirtualMachine.stop();
+ dropVm();
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ } catch (ServiceSpecificException e) {
+ throw new VirtualMachineException(e);
+ }
+ }
+ }
+
+ /**
+ * Stops this virtual machine, if it is running.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @see #stop()
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ @Override
+ public void close() {
+ synchronized (mLock) {
+ if (mVirtualMachine == null) {
+ return;
+ }
+ try {
+ if (stateToStatus(mVirtualMachine.getState()) == STATUS_RUNNING) {
+ mVirtualMachine.stop();
+ dropVm();
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ } catch (ServiceSpecificException e) {
+ // Deliberately ignored; this almost certainly means the VM exited just as
+ // we tried to stop it.
+ Log.i(TAG, "Ignoring error on close()", e);
+ }
+ }
+ }
+
+ private static void deleteRecursively(File dir) throws IOException {
+ // Note: This doesn't follow symlinks, which is important. Instead they are just deleted
+ // (and Files.delete deletes the link not the target).
+ Files.walkFileTree(dir.toPath(), new SimpleFileVisitor<>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+ throws IOException {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
+ // Directory is deleted after we've visited (deleted) all its contents, so it
+ // should be empty by now.
+ Files.delete(dir);
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+
+ /**
+ * 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.)
+ *
+ * <p>The new config must be {@linkplain VirtualMachineConfig#isCompatibleWith compatible with}
+ * the existing config.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @return the old config
+ * @throws VirtualMachineException if the virtual machine is not stopped, or the new config is
+ * incompatible.
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ @NonNull
+ public VirtualMachineConfig setConfig(@NonNull VirtualMachineConfig newConfig)
+ throws VirtualMachineException {
+ synchronized (mLock) {
+ VirtualMachineConfig oldConfig = mConfig;
+ if (!oldConfig.isCompatibleWith(newConfig)) {
+ throw new VirtualMachineException("incompatible config");
+ }
+ checkStopped();
+
+ if (oldConfig != newConfig) {
+ // Delete any existing file before recreating; that ensures any
+ // VirtualMachineDescriptor that refers to the old file does not see the new config.
+ mConfigFilePath.delete();
+ newConfig.serialize(mConfigFilePath);
+ mConfig = newConfig;
+ }
+ return oldConfig;
+ }
+ }
+
+ @Nullable
+ private static native IBinder nativeConnectToVsockServer(IBinder vmBinder, int port);
+
+ /**
+ * Connect to a VM's binder service via vsock and return the root IBinder object. Guest VMs are
+ * expected to set up vsock servers in their payload. After the host app receives the {@link
+ * VirtualMachineCallback#onPayloadReady}, it can use this method to establish a connection to
+ * the guest VM.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @throws VirtualMachineException if the virtual machine is not running or the connection
+ * failed.
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ @NonNull
+ public IBinder connectToVsockServer(
+ @IntRange(from = MIN_VSOCK_PORT, to = MAX_VSOCK_PORT) long port)
+ throws VirtualMachineException {
+
+ synchronized (mLock) {
+ IBinder iBinder =
+ nativeConnectToVsockServer(getRunningVm().asBinder(), validatePort(port));
+ if (iBinder == null) {
+ throw new VirtualMachineException("Failed to connect to vsock server");
+ }
+ return iBinder;
+ }
+ }
+
+ /**
+ * Opens a vsock connection to the VM on the given port.
+ *
+ * <p>The caller is responsible for closing the returned {@code ParcelFileDescriptor}.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @throws VirtualMachineException if connecting fails.
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ @NonNull
+ public ParcelFileDescriptor connectVsock(
+ @IntRange(from = MIN_VSOCK_PORT, to = MAX_VSOCK_PORT) long port)
+ throws VirtualMachineException {
+ synchronized (mLock) {
+ try {
+ return getRunningVm().connectVsock(validatePort(port));
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ } catch (ServiceSpecificException e) {
+ throw new VirtualMachineException(e);
+ }
+ }
+ }
+
+ private int validatePort(long port) {
+ // Ports below 1024 are "privileged" (payload code can't bind to these), and port numbers
+ // are 32-bit unsigned numbers at the OS level, even though we pass them as 32-bit signed
+ // numbers internally.
+ if (port < MIN_VSOCK_PORT || port > MAX_VSOCK_PORT) {
+ throw new IllegalArgumentException("Bad port " + port);
+ }
+ return (int) port;
+ }
+
+ /**
+ * Returns the root directory where all files related to this {@link VirtualMachine} (e.g.
+ * {@code instance.img}, {@code apk.idsig}, etc) are stored.
+ *
+ * @hide
+ */
+ @TestApi
+ @NonNull
+ public File getRootDir() {
+ return mVmRootPath;
+ }
+
+ /**
+ * Enables the VM to request attestation in testing mode.
+ *
+ * <p>This function provisions a key pair for the VM attestation testing, a fake certificate
+ * will be associated to the fake key pair when the VM requests attestation in testing mode.
+ *
+ * <p>The provisioned key pair can only be used in subsequent calls to {@link
+ * AVmPayload_requestAttestationForTesting} within a running VM.
+ *
+ * @hide
+ */
+ @TestApi
+ @RequiresPermission(USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ public void enableTestAttestation() throws VirtualMachineException {
+ try {
+ mVirtualizationService.getBinder().enableTestAttestation();
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
+ * Captures the current state of the VM in a {@link VirtualMachineDescriptor} instance. The VM
+ * needs to be stopped to avoid inconsistency in its state representation.
+ *
+ * <p>The state of the VM is not actually copied until {@link
+ * VirtualMachineManager#importFromDescriptor} is called. It is recommended that the VM not be
+ * started until that operation is complete.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @return a {@link VirtualMachineDescriptor} instance that represents the VM's state.
+ * @throws VirtualMachineException if the virtual machine is not stopped, or the state could not
+ * be captured.
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ @NonNull
+ public VirtualMachineDescriptor toDescriptor() throws VirtualMachineException {
+ synchronized (mLock) {
+ checkStopped();
+ try {
+ return new VirtualMachineDescriptor(
+ ParcelFileDescriptor.open(mConfigFilePath, MODE_READ_ONLY),
+ ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_ONLY),
+ mEncryptedStoreFilePath != null
+ ? ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_ONLY)
+ : null);
+ } catch (IOException e) {
+ throw new VirtualMachineException(e);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ VirtualMachineConfig config = getConfig();
+ String payloadConfigPath = config.getPayloadConfigPath();
+ String payloadBinaryName = config.getPayloadBinaryName();
+
+ StringBuilder result = new StringBuilder();
+ result.append("VirtualMachine(")
+ .append("name:")
+ .append(getName())
+ .append(", ");
+ if (payloadBinaryName != null) {
+ result.append("payload:").append(payloadBinaryName).append(", ");
+ }
+ if (payloadConfigPath != null) {
+ result.append("config:")
+ .append(payloadConfigPath)
+ .append(", ");
+ }
+ result.append("package: ")
+ .append(mPackageName)
+ .append(")");
+ return result.toString();
+ }
+
+ /**
+ * Reads the payload config inside the application, parses extra APK information, and then
+ * creates corresponding idsig file paths.
+ */
+ private static List<ExtraApkSpec> setupExtraApks(
+ @NonNull Context context, @NonNull VirtualMachineConfig config, @NonNull File vmDir)
+ throws VirtualMachineException {
+ String configPath = config.getPayloadConfigPath();
+ List<String> extraApks = config.getExtraApks();
+ if (configPath != null) {
+ return setupExtraApksFromConfigFile(context, vmDir, configPath);
+ } else if (!extraApks.isEmpty()) {
+ return setupExtraApksFromList(context, vmDir, extraApks);
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ private static List<ExtraApkSpec> setupExtraApksFromConfigFile(
+ Context context, File vmDir, String configPath) throws VirtualMachineException {
+ try (ZipFile zipFile = new ZipFile(context.getPackageCodePath())) {
+ InputStream inputStream = zipFile.getInputStream(zipFile.getEntry(configPath));
+ List<String> apkList =
+ parseExtraApkListFromPayloadConfig(
+ new JsonReader(new InputStreamReader(inputStream)));
+
+ List<ExtraApkSpec> extraApks = new ArrayList<>(apkList.size());
+ for (int i = 0; i < apkList.size(); ++i) {
+ extraApks.add(
+ new ExtraApkSpec(
+ new File(apkList.get(i)),
+ new File(vmDir, EXTRA_IDSIG_FILE_PREFIX + i)));
+ }
+
+ return extraApks;
+ } catch (IOException e) {
+ throw new VirtualMachineException("Couldn't parse extra apks from the vm config", e);
+ }
+ }
+
+ private static List<String> parseExtraApkListFromPayloadConfig(JsonReader reader)
+ throws VirtualMachineException {
+ /*
+ * JSON schema from packages/modules/Virtualization/microdroid/payload/config/src/lib.rs:
+ *
+ * <p>{ "extra_apks": [ { "path": "/system/app/foo.apk", }, ... ], ... }
+ */
+ try {
+ List<String> apks = new ArrayList<>();
+
+ reader.beginObject();
+ while (reader.hasNext()) {
+ if (reader.nextName().equals("extra_apks")) {
+ reader.beginArray();
+ while (reader.hasNext()) {
+ reader.beginObject();
+ String name = reader.nextName();
+ if (name.equals("path")) {
+ apks.add(reader.nextString());
+ } else {
+ reader.skipValue();
+ }
+ reader.endObject();
+ }
+ reader.endArray();
+ } else {
+ reader.skipValue();
+ }
+ }
+ reader.endObject();
+ return apks;
+ } catch (IOException e) {
+ throw new VirtualMachineException(e);
+ }
+ }
+
+ private static List<ExtraApkSpec> setupExtraApksFromList(
+ Context context, File vmDir, List<String> extraApkInfo) throws VirtualMachineException {
+ int count = extraApkInfo.size();
+ List<ExtraApkSpec> extraApks = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ String packageName = extraApkInfo.get(i);
+ ApplicationInfo appInfo;
+ try {
+ appInfo =
+ context.getPackageManager()
+ .getApplicationInfo(
+ packageName, PackageManager.ApplicationInfoFlags.of(0));
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new VirtualMachineException("Extra APK package not found", e);
+ }
+
+ extraApks.add(
+ new ExtraApkSpec(
+ new File(appInfo.sourceDir),
+ new File(vmDir, EXTRA_IDSIG_FILE_PREFIX + i)));
+ }
+ return extraApks;
+ }
+
+ private void importInstanceFrom(@NonNull ParcelFileDescriptor instanceFd)
+ throws VirtualMachineException {
+ try (FileChannel instance = new FileOutputStream(mInstanceFilePath).getChannel();
+ FileChannel instanceInput = new AutoCloseInputStream(instanceFd).getChannel()) {
+ instance.transferFrom(instanceInput, /*position=*/ 0, instanceInput.size());
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to transfer instance image", e);
+ }
+ }
+
+ private void importEncryptedStoreFrom(@NonNull ParcelFileDescriptor encryptedStoreFd)
+ throws VirtualMachineException {
+ try (FileChannel storeOutput = new FileOutputStream(mEncryptedStoreFilePath).getChannel();
+ FileChannel storeInput = new AutoCloseInputStream(encryptedStoreFd).getChannel()) {
+ storeOutput.transferFrom(storeInput, /*position=*/ 0, storeInput.size());
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to transfer encryptedstore image", e);
+ }
+ }
+
+ /** Map the raw AIDL (& binder) callbacks to what the client expects. */
+ private class CallbackTranslator extends IVirtualMachineCallback.Stub {
+ private final IVirtualizationService mService;
+ private final DeathRecipient mDeathRecipient;
+
+ // The VM should only be observed to die once
+ private final AtomicBoolean mOnDiedCalled = new AtomicBoolean(false);
+
+ public CallbackTranslator(IVirtualizationService service) throws RemoteException {
+ this.mService = service;
+ this.mDeathRecipient = () -> reportStopped(STOP_REASON_VIRTUALIZATION_SERVICE_DIED);
+ service.asBinder().linkToDeath(mDeathRecipient, 0);
+ }
+
+ @Override
+ public void onPayloadStarted(int cid) {
+ executeCallback((cb) -> cb.onPayloadStarted(VirtualMachine.this));
+ }
+
+ @Override
+ public void onPayloadReady(int cid) {
+ executeCallback((cb) -> cb.onPayloadReady(VirtualMachine.this));
+ }
+
+ @Override
+ public void onPayloadFinished(int cid, int exitCode) {
+ executeCallback((cb) -> cb.onPayloadFinished(VirtualMachine.this, exitCode));
+ }
+
+ @Override
+ public void onError(int cid, int errorCode, String message) {
+ int translatedError = getTranslatedError(errorCode);
+ executeCallback((cb) -> cb.onError(VirtualMachine.this, translatedError, message));
+ }
+
+ @Override
+ public void onDied(int cid, int reason) {
+ int translatedReason = getTranslatedReason(reason);
+ reportStopped(translatedReason);
+ mService.asBinder().unlinkToDeath(mDeathRecipient, 0);
+ }
+
+ private void reportStopped(@VirtualMachineCallback.StopReason int reason) {
+ if (mOnDiedCalled.compareAndSet(false, true)) {
+ executeCallback((cb) -> cb.onStopped(VirtualMachine.this, reason));
+ }
+ }
+
+ @VirtualMachineCallback.ErrorCode
+ private int getTranslatedError(int reason) {
+ switch (reason) {
+ case ErrorCode.PAYLOAD_VERIFICATION_FAILED:
+ return ERROR_PAYLOAD_VERIFICATION_FAILED;
+ case ErrorCode.PAYLOAD_CHANGED:
+ return ERROR_PAYLOAD_CHANGED;
+ case ErrorCode.PAYLOAD_INVALID_CONFIG:
+ return ERROR_PAYLOAD_INVALID_CONFIG;
+ default:
+ return ERROR_UNKNOWN;
+ }
+ }
+
+ @VirtualMachineCallback.StopReason
+ private int getTranslatedReason(int reason) {
+ switch (reason) {
+ case DeathReason.INFRASTRUCTURE_ERROR:
+ return STOP_REASON_INFRASTRUCTURE_ERROR;
+ case DeathReason.KILLED:
+ return STOP_REASON_KILLED;
+ case DeathReason.SHUTDOWN:
+ return STOP_REASON_SHUTDOWN;
+ case DeathReason.START_FAILED:
+ return STOP_REASON_START_FAILED;
+ case DeathReason.REBOOT:
+ return STOP_REASON_REBOOT;
+ case DeathReason.CRASH:
+ return STOP_REASON_CRASH;
+ case DeathReason.PVM_FIRMWARE_PUBLIC_KEY_MISMATCH:
+ return STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH;
+ case DeathReason.PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED:
+ return STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED;
+ case DeathReason.MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE:
+ return STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE;
+ case DeathReason.MICRODROID_PAYLOAD_HAS_CHANGED:
+ return STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED;
+ case DeathReason.MICRODROID_PAYLOAD_VERIFICATION_FAILED:
+ return STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED;
+ case DeathReason.MICRODROID_INVALID_PAYLOAD_CONFIG:
+ return STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG;
+ case DeathReason.MICRODROID_UNKNOWN_RUNTIME_ERROR:
+ return STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR;
+ case DeathReason.HANGUP:
+ return STOP_REASON_HANGUP;
+ default:
+ return STOP_REASON_UNKNOWN;
+ }
+ }
+ }
+}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCallback.java b/java/framework/src/android/system/virtualmachine/VirtualMachineCallback.java
new file mode 100644
index 0000000..d72ba14
--- /dev/null
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -0,0 +1,157 @@
+/*
+ * 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.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Callback interface to get notified with the events from the virtual machine. The methods are
+ * executed on a binder thread. Implementations can make blocking calls in the methods.
+ *
+ * @hide
+ */
+@SystemApi
+@SuppressLint("CallbackInterface") // Guidance has changed, lint is out of date (b/245552641)
+public interface VirtualMachineCallback {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "ERROR_", value = {
+ ERROR_UNKNOWN,
+ ERROR_PAYLOAD_VERIFICATION_FAILED,
+ ERROR_PAYLOAD_CHANGED,
+ ERROR_PAYLOAD_INVALID_CONFIG
+ })
+ @interface ErrorCode {}
+
+ /** Error code for all other errors not listed below. */
+ int ERROR_UNKNOWN = 0;
+ /**
+ * Error code indicating that the payload can't be verified due to various reasons (e.g invalid
+ * merkle tree, invalid formats, etc).
+ */
+ int ERROR_PAYLOAD_VERIFICATION_FAILED = 1;
+ /** Error code indicating that the payload is verified, but has changed since the last boot. */
+ int ERROR_PAYLOAD_CHANGED = 2;
+ /** Error code indicating that the payload config is invalid. */
+ int ERROR_PAYLOAD_INVALID_CONFIG = 3;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = "STOP_REASON_",
+ value = {
+ STOP_REASON_VIRTUALIZATION_SERVICE_DIED,
+ STOP_REASON_INFRASTRUCTURE_ERROR,
+ STOP_REASON_KILLED,
+ STOP_REASON_UNKNOWN,
+ STOP_REASON_SHUTDOWN,
+ STOP_REASON_START_FAILED,
+ STOP_REASON_REBOOT,
+ STOP_REASON_CRASH,
+ STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH,
+ STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED,
+ STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH,
+ STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED,
+ STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE,
+ STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED,
+ STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED,
+ STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG,
+ STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR,
+ STOP_REASON_HANGUP,
+ })
+ @interface StopReason {}
+
+ /** The virtualization service itself died, taking the VM down with it. */
+ // This is a negative number to avoid conflicting with the other death reasons which match
+ // the ones in the AIDL interface.
+ int STOP_REASON_VIRTUALIZATION_SERVICE_DIED = -1;
+
+ /** There was an error waiting for the VM. */
+ int STOP_REASON_INFRASTRUCTURE_ERROR = 0;
+
+ /** The VM was killed. */
+ int STOP_REASON_KILLED = 1;
+
+ /** The VM died for an unknown reason. */
+ int STOP_REASON_UNKNOWN = 2;
+
+ /** The VM requested to shut down. */
+ int STOP_REASON_SHUTDOWN = 3;
+
+ /** crosvm had an error starting the VM. */
+ int STOP_REASON_START_FAILED = 4;
+
+ /** The VM requested to reboot, possibly as the result of a kernel panic. */
+ int STOP_REASON_REBOOT = 5;
+
+ /** The VM or crosvm crashed. */
+ int STOP_REASON_CRASH = 6;
+
+ /** The pVM firmware failed to verify the VM because the public key doesn't match. */
+ int STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH = 7;
+
+ /** The pVM firmware failed to verify the VM because the instance image changed. */
+ int STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED = 8;
+
+ /** The bootloader failed to verify the VM because the public key doesn't match. */
+ int STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH = 9;
+
+ /** The bootloader failed to verify the VM because the instance image changed. */
+ int STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED = 10;
+
+ /** The microdroid failed to connect to VirtualizationService's RPC server. */
+ int STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE = 11;
+
+ /** The payload for microdroid is changed. */
+ int STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED = 12;
+
+ /** The microdroid failed to verify given payload APK. */
+ int STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED = 13;
+
+ /** The VM config for microdroid is invalid (e.g. missing tasks). */
+ int STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG = 14;
+
+ /** There was a runtime error while running microdroid manager. */
+ int STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR = 15;
+
+ /** The VM killed due to hangup */
+ int STOP_REASON_HANGUP = 16;
+
+ /** Called when the payload starts in the VM. */
+ void onPayloadStarted(@NonNull VirtualMachine vm);
+
+ /**
+ * Called when the payload in the VM is ready to serve. See {@link
+ * VirtualMachine#connectToVsockServer}.
+ */
+ void onPayloadReady(@NonNull VirtualMachine vm);
+
+ /** Called when the payload has finished in the VM. */
+ void onPayloadFinished(@NonNull VirtualMachine vm, int exitCode);
+
+ /** Called when an error occurs in the VM. */
+ void onError(@NonNull VirtualMachine vm, @ErrorCode int errorCode, @NonNull String message);
+
+ /** Called when the VM has stopped. */
+ void onStopped(@NonNull VirtualMachine vm, @StopReason int reason);
+}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
new file mode 100644
index 0000000..693a7d7
--- /dev/null
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -0,0 +1,1033 @@
+/*
+ * 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.AutoCloseInputStream;
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.sysprop.HypervisorProperties;
+import android.system.virtualizationservice.VirtualMachineAppConfig;
+import android.system.virtualizationservice.VirtualMachinePayloadConfig;
+import android.util.Log;
+
+import com.android.system.virtualmachine.flags.Flags;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.zip.ZipFile;
+
+/**
+ * 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
+ * payload to run on the virtual machine.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualMachineConfig {
+ private static final String TAG = "VirtualMachineConfig";
+
+ private static String[] EMPTY_STRING_ARRAY = {};
+
+ // These define the schema of the config file persisted on disk.
+ private static final int VERSION = 8;
+ private static final String KEY_VERSION = "version";
+ private static final String KEY_PACKAGENAME = "packageName";
+ private static final String KEY_APKPATH = "apkPath";
+ private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath";
+ private static final String KEY_PAYLOADBINARYNAME = "payloadBinaryPath";
+ private static final String KEY_DEBUGLEVEL = "debugLevel";
+ private static final String KEY_PROTECTED_VM = "protectedVm";
+ private static final String KEY_MEMORY_BYTES = "memoryBytes";
+ private static final String KEY_CPU_TOPOLOGY = "cpuTopology";
+ private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
+ private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
+ private static final String KEY_VM_CONSOLE_INPUT_SUPPORTED = "vmConsoleInputSupported";
+ private static final String KEY_VENDOR_DISK_IMAGE_PATH = "vendorDiskImagePath";
+ private static final String KEY_OS = "os";
+ private static final String KEY_EXTRA_APKS = "extraApks";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "DEBUG_LEVEL_", value = {
+ DEBUG_LEVEL_NONE,
+ DEBUG_LEVEL_FULL
+ })
+ public @interface DebugLevel {}
+
+ /**
+ * Not debuggable at all. No log is exported from the VM. Debugger can't be attached to the app
+ * process running in the VM. This is the default level.
+ *
+ * @hide
+ */
+ @SystemApi public static final int DEBUG_LEVEL_NONE = 0;
+
+ /**
+ * Fully debuggable. All logs (both logcat and kernel message) are exported. All processes
+ * running in the VM can be attached to the debugger. Rooting is possible.
+ *
+ * @hide
+ */
+ @SystemApi public static final int DEBUG_LEVEL_FULL = 1;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = "CPU_TOPOLOGY_",
+ value = {
+ CPU_TOPOLOGY_ONE_CPU,
+ CPU_TOPOLOGY_MATCH_HOST,
+ })
+ public @interface CpuTopology {}
+
+ /**
+ * Run VM with 1 vCPU. This is the default option, usually the fastest to boot and consuming the
+ * least amount of resources. Typically the best option for small or ephemeral workloads.
+ *
+ * @hide
+ */
+ @SystemApi public static final int CPU_TOPOLOGY_ONE_CPU = 0;
+
+ /**
+ * Run VM with vCPU topology matching the physical CPU topology of the host. Usually takes
+ * longer to boot and consumes more resources compared to a single vCPU. Typically a good option
+ * for long-running workloads that benefit from parallel execution.
+ *
+ * @hide
+ */
+ @SystemApi public static final int CPU_TOPOLOGY_MATCH_HOST = 1;
+
+ /** Name of a package whose primary APK contains the VM payload. */
+ @Nullable private final String mPackageName;
+
+ /** Absolute path to the APK file containing the VM payload. */
+ @Nullable private final String mApkPath;
+
+ private final List<String> mExtraApks;
+
+ @DebugLevel private final int mDebugLevel;
+
+ /**
+ * Whether to run the VM in protected mode, so the host can't access its memory.
+ */
+ private final boolean mProtectedVm;
+
+ /**
+ * The amount of RAM to give the VM, in bytes. If this is 0 or negative the default will be
+ * used.
+ */
+ private final long mMemoryBytes;
+
+ /** CPU topology configuration of the VM. */
+ @CpuTopology private final int mCpuTopology;
+
+ /**
+ * Path within the APK to the payload config file that defines software aspects of the VM.
+ */
+ @Nullable private final String mPayloadConfigPath;
+
+ /** Name of the payload binary file within the APK that will be executed within the VM. */
+ @Nullable private final String mPayloadBinaryName;
+
+ /** The size of storage in bytes. 0 indicates that encryptedStorage is not required */
+ private final long mEncryptedStorageBytes;
+
+ /** Whether the app can read console and log output. */
+ private final boolean mVmOutputCaptured;
+
+ /** Whether the app can write console input to the VM */
+ private final boolean mVmConsoleInputSupported;
+
+ @Nullable private final File mVendorDiskImage;
+
+ /** OS name of the VM using payload binaries. null if the VM uses a payload config file. */
+ @Nullable private final String mOs;
+
+ private VirtualMachineConfig(
+ @Nullable String packageName,
+ @Nullable String apkPath,
+ List<String> extraApks,
+ @Nullable String payloadConfigPath,
+ @Nullable String payloadBinaryName,
+ @DebugLevel int debugLevel,
+ boolean protectedVm,
+ long memoryBytes,
+ @CpuTopology int cpuTopology,
+ long encryptedStorageBytes,
+ boolean vmOutputCaptured,
+ boolean vmConsoleInputSupported,
+ @Nullable File vendorDiskImage,
+ @Nullable String os) {
+ // This is only called from Builder.build(); the builder handles parameter validation.
+ mPackageName = packageName;
+ mApkPath = apkPath;
+ mExtraApks =
+ extraApks.isEmpty()
+ ? Collections.emptyList()
+ : Collections.unmodifiableList(
+ Arrays.asList(extraApks.toArray(new String[0])));
+ mPayloadConfigPath = payloadConfigPath;
+ mPayloadBinaryName = payloadBinaryName;
+ mDebugLevel = debugLevel;
+ mProtectedVm = protectedVm;
+ mMemoryBytes = memoryBytes;
+ mCpuTopology = cpuTopology;
+ mEncryptedStorageBytes = encryptedStorageBytes;
+ mVmOutputCaptured = vmOutputCaptured;
+ mVmConsoleInputSupported = vmConsoleInputSupported;
+ mVendorDiskImage = vendorDiskImage;
+ mOs = os;
+ }
+
+ /** Loads a config from a file. */
+ @NonNull
+ static VirtualMachineConfig from(@NonNull File file) throws VirtualMachineException {
+ try (FileInputStream input = new FileInputStream(file)) {
+ return fromInputStream(input);
+ } catch (IOException e) {
+ throw new VirtualMachineException("Failed to read VM config from file", e);
+ }
+ }
+
+ /** Loads a config from a {@link ParcelFileDescriptor}. */
+ @NonNull
+ static VirtualMachineConfig from(@NonNull ParcelFileDescriptor fd)
+ throws VirtualMachineException {
+ try (AutoCloseInputStream input = new AutoCloseInputStream(fd)) {
+ return fromInputStream(input);
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to read VM config from file descriptor", e);
+ }
+ }
+
+ /** Loads a config from a stream, for example a file. */
+ @NonNull
+ private static VirtualMachineConfig fromInputStream(@NonNull InputStream input)
+ throws IOException, VirtualMachineException {
+ PersistableBundle b = PersistableBundle.readFromStream(input);
+ try {
+ return fromPersistableBundle(b);
+ } catch (NullPointerException | IllegalArgumentException | IllegalStateException e) {
+ throw new VirtualMachineException("Persisted VM config is invalid", e);
+ }
+ }
+
+ @NonNull
+ private static VirtualMachineConfig fromPersistableBundle(PersistableBundle b) {
+ int version = b.getInt(KEY_VERSION);
+ if (version > VERSION) {
+ throw new IllegalArgumentException(
+ "Version " + version + " too high; current is " + VERSION);
+ }
+
+ String packageName = b.getString(KEY_PACKAGENAME);
+ Builder builder = new Builder(packageName);
+
+ String apkPath = b.getString(KEY_APKPATH);
+ if (apkPath != null) {
+ builder.setApkPath(apkPath);
+ }
+
+ String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH);
+ if (payloadConfigPath == null) {
+ builder.setPayloadBinaryName(b.getString(KEY_PAYLOADBINARYNAME));
+ } else {
+ builder.setPayloadConfigPath(payloadConfigPath);
+ }
+
+ @DebugLevel int debugLevel = b.getInt(KEY_DEBUGLEVEL);
+ if (debugLevel != DEBUG_LEVEL_NONE && debugLevel != DEBUG_LEVEL_FULL) {
+ throw new IllegalArgumentException("Invalid debugLevel: " + debugLevel);
+ }
+ builder.setDebugLevel(debugLevel);
+ builder.setProtectedVm(b.getBoolean(KEY_PROTECTED_VM));
+ long memoryBytes = b.getLong(KEY_MEMORY_BYTES);
+ if (memoryBytes != 0) {
+ builder.setMemoryBytes(memoryBytes);
+ }
+ builder.setCpuTopology(b.getInt(KEY_CPU_TOPOLOGY));
+ long encryptedStorageBytes = b.getLong(KEY_ENCRYPTED_STORAGE_BYTES);
+ if (encryptedStorageBytes != 0) {
+ builder.setEncryptedStorageBytes(encryptedStorageBytes);
+ }
+ builder.setVmOutputCaptured(b.getBoolean(KEY_VM_OUTPUT_CAPTURED));
+ builder.setVmConsoleInputSupported(b.getBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED));
+
+ String vendorDiskImagePath = b.getString(KEY_VENDOR_DISK_IMAGE_PATH);
+ if (vendorDiskImagePath != null) {
+ builder.setVendorDiskImage(new File(vendorDiskImagePath));
+ }
+
+ String os = b.getString(KEY_OS);
+ if (os != null) {
+ builder.setOs(os);
+ }
+
+ String[] extraApks = b.getStringArray(KEY_EXTRA_APKS);
+ if (extraApks != null) {
+ for (String extraApk : extraApks) {
+ builder.addExtraApk(extraApk);
+ }
+ }
+
+ return builder.build();
+ }
+
+ /** Persists this config to a file. */
+ void serialize(@NonNull File file) throws VirtualMachineException {
+ try (FileOutputStream output = new FileOutputStream(file)) {
+ serializeOutputStream(output);
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to write VM config", e);
+ }
+ }
+
+ /** Persists this config to a stream, for example a file. */
+ private void serializeOutputStream(@NonNull OutputStream output) throws IOException {
+ PersistableBundle b = new PersistableBundle();
+ b.putInt(KEY_VERSION, VERSION);
+ if (mPackageName != null) {
+ b.putString(KEY_PACKAGENAME, mPackageName);
+ }
+ if (mApkPath != null) {
+ b.putString(KEY_APKPATH, mApkPath);
+ }
+ b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath);
+ b.putString(KEY_PAYLOADBINARYNAME, mPayloadBinaryName);
+ b.putInt(KEY_DEBUGLEVEL, mDebugLevel);
+ b.putBoolean(KEY_PROTECTED_VM, mProtectedVm);
+ b.putInt(KEY_CPU_TOPOLOGY, mCpuTopology);
+ if (mMemoryBytes > 0) {
+ b.putLong(KEY_MEMORY_BYTES, mMemoryBytes);
+ }
+ if (mEncryptedStorageBytes > 0) {
+ b.putLong(KEY_ENCRYPTED_STORAGE_BYTES, mEncryptedStorageBytes);
+ }
+ b.putBoolean(KEY_VM_OUTPUT_CAPTURED, mVmOutputCaptured);
+ b.putBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED, mVmConsoleInputSupported);
+ if (mVendorDiskImage != null) {
+ b.putString(KEY_VENDOR_DISK_IMAGE_PATH, mVendorDiskImage.getAbsolutePath());
+ }
+ b.putString(KEY_OS, mOs);
+ if (!mExtraApks.isEmpty()) {
+ String[] extraApks = mExtraApks.toArray(new String[0]);
+ b.putStringArray(KEY_EXTRA_APKS, extraApks);
+ }
+ b.writeToStream(output);
+ }
+
+ /**
+ * Returns the absolute path of the APK which should contain the binary payload that will
+ * execute within the VM. Returns null if no specific path has been set.
+ *
+ * @hide
+ */
+ @SystemApi
+ @Nullable
+ public String getApkPath() {
+ return mApkPath;
+ }
+
+ /**
+ * Returns the package names of any extra APKs that have been requested for the VM. They are
+ * returned in the order in which they were added via {@link Builder#addExtraApk}.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ @NonNull
+ public List<String> getExtraApks() {
+ return mExtraApks;
+ }
+
+ /**
+ * Returns the path within the APK to the payload config file that defines software aspects of
+ * the VM.
+ *
+ * @hide
+ */
+ @TestApi
+ @Nullable
+ public String getPayloadConfigPath() {
+ return mPayloadConfigPath;
+ }
+
+ /**
+ * Returns the name of the payload binary file, in the {@code lib/<ABI>} directory of the APK,
+ * that will be executed within the VM.
+ *
+ * @hide
+ */
+ @SystemApi
+ @Nullable
+ public String getPayloadBinaryName() {
+ return mPayloadBinaryName;
+ }
+
+ /**
+ * Returns the debug level for the VM.
+ *
+ * @hide
+ */
+ @SystemApi
+ @DebugLevel
+ public int getDebugLevel() {
+ return mDebugLevel;
+ }
+
+ /**
+ * Returns whether the VM's memory will be protected from the host.
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean isProtectedVm() {
+ return mProtectedVm;
+ }
+
+ /**
+ * Returns the amount of RAM that will be made available to the VM, or 0 if the default size
+ * will be used.
+ *
+ * @hide
+ */
+ @SystemApi
+ @IntRange(from = 0)
+ public long getMemoryBytes() {
+ return mMemoryBytes;
+ }
+
+ /**
+ * Returns the CPU topology configuration of the VM.
+ *
+ * @hide
+ */
+ @SystemApi
+ @CpuTopology
+ public int getCpuTopology() {
+ return mCpuTopology;
+ }
+
+ /**
+ * Returns whether encrypted storage is enabled or not.
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean isEncryptedStorageEnabled() {
+ return mEncryptedStorageBytes > 0;
+ }
+
+ /**
+ * Returns the size of encrypted storage (in bytes) available in the VM, or 0 if encrypted
+ * storage is not enabled
+ *
+ * @hide
+ */
+ @SystemApi
+ @IntRange(from = 0)
+ public long getEncryptedStorageBytes() {
+ return mEncryptedStorageBytes;
+ }
+
+ /**
+ * Returns whether the app can read the VM console or log output. If not, the VM output is
+ * automatically forwarded to the host logcat.
+ *
+ * @see Builder#setVmOutputCaptured
+ * @hide
+ */
+ @SystemApi
+ public boolean isVmOutputCaptured() {
+ return mVmOutputCaptured;
+ }
+
+ /**
+ * Returns whether the app can write to the VM console.
+ *
+ * @see Builder#setVmConsoleInputSupported
+ * @hide
+ */
+ @TestApi
+ public boolean isVmConsoleInputSupported() {
+ return mVmConsoleInputSupported;
+ }
+
+ /**
+ * Returns the OS of the VM using a payload binary. Returns null if the VM uses payload config.
+ *
+ * @see Builder#setOs
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ @Nullable
+ public String getOs() {
+ return mOs;
+ }
+
+ /**
+ * Tests if this config is compatible with other config. Being compatible means that the configs
+ * can be interchangeably used for the same virtual machine; they do not change the VM identity
+ * or secrets. Such changes include varying the number of CPUs or the size of the RAM. Changes
+ * that would alter the identity of the VM (e.g. using a different payload or changing the debug
+ * mode) are considered incompatible.
+ *
+ * @see VirtualMachine#setConfig
+ * @hide
+ */
+ @SystemApi
+ public boolean isCompatibleWith(@NonNull VirtualMachineConfig other) {
+ if (this == other) {
+ return true;
+ }
+ return this.mDebugLevel == other.mDebugLevel
+ && this.mProtectedVm == other.mProtectedVm
+ && this.mEncryptedStorageBytes == other.mEncryptedStorageBytes
+ && this.mVmOutputCaptured == other.mVmOutputCaptured
+ && this.mVmConsoleInputSupported == other.mVmConsoleInputSupported
+ && (this.mVendorDiskImage == null) == (other.mVendorDiskImage == null)
+ && Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath)
+ && Objects.equals(this.mPayloadBinaryName, other.mPayloadBinaryName)
+ && Objects.equals(this.mPackageName, other.mPackageName)
+ && Objects.equals(this.mOs, other.mOs)
+ && Objects.equals(this.mExtraApks, other.mExtraApks);
+ }
+
+ /**
+ * Converts this config object into the parcelable type 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.
+ */
+ VirtualMachineAppConfig toVsConfig(@NonNull PackageManager packageManager)
+ throws VirtualMachineException {
+ VirtualMachineAppConfig vsConfig = new VirtualMachineAppConfig();
+
+ String apkPath = (mApkPath != null) ? mApkPath : findPayloadApk(packageManager);
+
+ try {
+ vsConfig.apk = ParcelFileDescriptor.open(new File(apkPath), MODE_READ_ONLY);
+ } catch (FileNotFoundException e) {
+ throw new VirtualMachineException("Failed to open APK", e);
+ }
+ if (mPayloadBinaryName != null) {
+ VirtualMachinePayloadConfig payloadConfig = new VirtualMachinePayloadConfig();
+ payloadConfig.payloadBinaryName = mPayloadBinaryName;
+ payloadConfig.osName = mOs;
+ payloadConfig.extraApks = Collections.emptyList();
+ vsConfig.payload =
+ VirtualMachineAppConfig.Payload.payloadConfig(payloadConfig);
+ } else {
+ vsConfig.payload =
+ VirtualMachineAppConfig.Payload.configPath(mPayloadConfigPath);
+ }
+ switch (mDebugLevel) {
+ case DEBUG_LEVEL_FULL:
+ vsConfig.debugLevel = VirtualMachineAppConfig.DebugLevel.FULL;
+ break;
+ default:
+ vsConfig.debugLevel = VirtualMachineAppConfig.DebugLevel.NONE;
+ break;
+ }
+ vsConfig.protectedVm = mProtectedVm;
+ vsConfig.memoryMib = bytesToMebiBytes(mMemoryBytes);
+ switch (mCpuTopology) {
+ case CPU_TOPOLOGY_MATCH_HOST:
+ vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.MATCH_HOST;
+ break;
+ default:
+ vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.ONE_CPU;
+ break;
+ }
+ if (mVendorDiskImage != null) {
+ VirtualMachineAppConfig.CustomConfig customConfig =
+ new VirtualMachineAppConfig.CustomConfig();
+ customConfig.devices = EMPTY_STRING_ARRAY;
+ try {
+ customConfig.vendorImage =
+ ParcelFileDescriptor.open(mVendorDiskImage, MODE_READ_ONLY);
+ } catch (FileNotFoundException e) {
+ throw new VirtualMachineException(
+ "Failed to open vendor disk image " + mVendorDiskImage.getAbsolutePath(),
+ e);
+ }
+ vsConfig.customConfig = customConfig;
+ }
+ return vsConfig;
+ }
+
+ private String findPayloadApk(PackageManager packageManager) throws VirtualMachineException {
+ ApplicationInfo appInfo;
+ try {
+ appInfo =
+ packageManager.getApplicationInfo(
+ mPackageName, PackageManager.ApplicationInfoFlags.of(0));
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new VirtualMachineException("Package not found", e);
+ }
+
+ String[] splitApkPaths = appInfo.splitSourceDirs;
+ String[] abis = Build.SUPPORTED_64_BIT_ABIS;
+
+ // If there are split APKs, and we know the payload binary name, see if we can find a
+ // split APK containing the binary.
+ if (mPayloadBinaryName != null && splitApkPaths != null && abis.length != 0) {
+ String[] libraryNames = new String[abis.length];
+ for (int i = 0; i < abis.length; i++) {
+ libraryNames[i] = "lib/" + abis[i] + "/" + mPayloadBinaryName;
+ }
+
+ for (String path : splitApkPaths) {
+ try (ZipFile zip = new ZipFile(path)) {
+ for (String name : libraryNames) {
+ if (zip.getEntry(name) != null) {
+ Log.i(TAG, "Found payload in " + path);
+ return path;
+ }
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to scan split APK: " + path, e);
+ }
+ }
+ }
+
+ // This really is the path to the APK, not a directory.
+ return appInfo.sourceDir;
+ }
+
+ private int bytesToMebiBytes(long mMemoryBytes) {
+ long oneMebi = 1024 * 1024;
+ // We can't express requests for more than 2 exabytes, but then they're not going to succeed
+ // anyway.
+ if (mMemoryBytes > (Integer.MAX_VALUE - 1) * oneMebi) {
+ return Integer.MAX_VALUE;
+ }
+ return (int) ((mMemoryBytes + oneMebi - 1) / oneMebi);
+ }
+
+ /**
+ * A builder used to create a {@link VirtualMachineConfig}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final class Builder {
+ private final String DEFAULT_OS = "microdroid";
+
+ @Nullable private final String mPackageName;
+ @Nullable private String mApkPath;
+ private final List<String> mExtraApks = new ArrayList<>();
+ @Nullable private String mPayloadConfigPath;
+ @Nullable private String mPayloadBinaryName;
+ @DebugLevel private int mDebugLevel = DEBUG_LEVEL_NONE;
+ private boolean mProtectedVm;
+ private boolean mProtectedVmSet;
+ private long mMemoryBytes;
+ @CpuTopology private int mCpuTopology = CPU_TOPOLOGY_ONE_CPU;
+ private long mEncryptedStorageBytes;
+ private boolean mVmOutputCaptured = false;
+ private boolean mVmConsoleInputSupported = false;
+ @Nullable private File mVendorDiskImage;
+ @Nullable private String mOs;
+
+ /**
+ * Creates a builder for the given context.
+ *
+ * @hide
+ */
+ @SystemApi
+ public Builder(@NonNull Context context) {
+ mPackageName = requireNonNull(context, "context must not be null").getPackageName();
+ }
+
+ /**
+ * Creates a builder for a specific package. If packageName is null, {@link #setApkPath}
+ * must be called to specify the APK containing the payload.
+ */
+ private Builder(@Nullable String packageName) {
+ mPackageName = packageName;
+ }
+
+ /**
+ * Builds an immutable {@link VirtualMachineConfig}
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public VirtualMachineConfig build() {
+ String apkPath = null;
+ String packageName = null;
+
+ if (mApkPath != null) {
+ apkPath = mApkPath;
+ } else if (mPackageName != null) {
+ packageName = mPackageName;
+ } else {
+ // This should never happen, unless we're deserializing a bad config
+ throw new IllegalStateException("apkPath or packageName must be specified");
+ }
+
+ String os = null;
+ if (mPayloadBinaryName == null) {
+ if (mPayloadConfigPath == null) {
+ throw new IllegalStateException("setPayloadBinaryName must be called");
+ }
+ if (mOs != null) {
+ throw new IllegalStateException(
+ "setPayloadConfigPath and setOs may not both be called");
+ }
+ if (!mExtraApks.isEmpty()) {
+ throw new IllegalStateException(
+ "setPayloadConfigPath and addExtraApk may not both be called");
+ }
+ } else {
+ if (mPayloadConfigPath != null) {
+ throw new IllegalStateException(
+ "setPayloadBinaryName and setPayloadConfigPath may not both be called");
+ }
+ if (mOs != null) {
+ os = mOs;
+ } else {
+ os = DEFAULT_OS;
+ }
+ }
+
+ if (!mProtectedVmSet) {
+ throw new IllegalStateException("setProtectedVm must be called explicitly");
+ }
+
+ if (mVmOutputCaptured && mDebugLevel != DEBUG_LEVEL_FULL) {
+ throw new IllegalStateException("debug level must be FULL to capture output");
+ }
+
+ if (mVmConsoleInputSupported && mDebugLevel != DEBUG_LEVEL_FULL) {
+ throw new IllegalStateException("debug level must be FULL to use console input");
+ }
+
+ return new VirtualMachineConfig(
+ packageName,
+ apkPath,
+ mExtraApks,
+ mPayloadConfigPath,
+ mPayloadBinaryName,
+ mDebugLevel,
+ mProtectedVm,
+ mMemoryBytes,
+ mCpuTopology,
+ mEncryptedStorageBytes,
+ mVmOutputCaptured,
+ mVmConsoleInputSupported,
+ mVendorDiskImage,
+ os);
+ }
+
+ /**
+ * Sets the absolute path of the APK containing the binary payload that will execute within
+ * the VM. If not set explicitly, defaults to the split APK containing the payload, if there
+ * is one, and otherwise the primary APK of the context.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public Builder setApkPath(@NonNull String apkPath) {
+ requireNonNull(apkPath, "apkPath must not be null");
+ if (!apkPath.startsWith("/")) {
+ throw new IllegalArgumentException("APK path must be an absolute path");
+ }
+ mApkPath = apkPath;
+ return this;
+ }
+
+ /**
+ * Specify the package name of an extra APK to be included in the VM. Each extra APK is
+ * mounted, in unzipped form, inside the VM, allowing access to the code and/or data within
+ * it. The VM entry point must be in the main APK.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ @NonNull
+ public Builder addExtraApk(@NonNull String packageName) {
+ mExtraApks.add(requireNonNull(packageName, "extra APK package name must not be null"));
+ return this;
+ }
+
+ /**
+ * Sets the path within the APK to the payload config file that defines software aspects of
+ * the VM. The file is a JSON file; see
+ * packages/modules/Virtualization/microdroid/payload/config/src/lib.rs for the format.
+ *
+ * @hide
+ */
+ @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+ @TestApi
+ @NonNull
+ public Builder setPayloadConfigPath(@NonNull String payloadConfigPath) {
+ mPayloadConfigPath =
+ requireNonNull(payloadConfigPath, "payloadConfigPath must not be null");
+ return this;
+ }
+
+ /**
+ * Sets the name of the payload binary file that will be executed within the VM, e.g.
+ * "payload.so". The file must reside in the {@code lib/<ABI>} directory of the APK.
+ *
+ * <p>Note that VMs only support 64-bit code, even if the owning app is running as a 32-bit
+ * process.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public Builder setPayloadBinaryName(@NonNull String payloadBinaryName) {
+ if (payloadBinaryName.contains(File.separator)) {
+ throw new IllegalArgumentException(
+ "Invalid binary file name: " + payloadBinaryName);
+ }
+ mPayloadBinaryName =
+ requireNonNull(payloadBinaryName, "payloadBinaryName must not be null");
+ return this;
+ }
+
+ /**
+ * Sets the debug level. Defaults to {@link #DEBUG_LEVEL_NONE}.
+ *
+ * <p>If {@link #DEBUG_LEVEL_FULL} is set then logs from inside the VM are exported to the
+ * host and adb connections from the host are possible. This is convenient for debugging but
+ * may compromise the integrity of the VM - including bypassing the protections offered by a
+ * {@linkplain #setProtectedVm protected VM}.
+ *
+ * <p>Note that it isn't possible to {@linkplain #isCompatibleWith change} the debug level
+ * of a VM instance; debug and non-debug VMs always have different secrets.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public Builder setDebugLevel(@DebugLevel int debugLevel) {
+ if (debugLevel != DEBUG_LEVEL_NONE && debugLevel != DEBUG_LEVEL_FULL) {
+ throw new IllegalArgumentException("Invalid debugLevel: " + debugLevel);
+ }
+ mDebugLevel = debugLevel;
+ return this;
+ }
+
+ /**
+ * Sets whether to protect the VM memory from the host. No default is provided, this must be
+ * set explicitly.
+ *
+ * <p>Note that if debugging is {@linkplain #setDebugLevel enabled} for a protected VM, the
+ * VM is not truly protected - direct memory access by the host is prevented, but e.g. the
+ * debugger can be used to access the VM's internals.
+ *
+ * <p>It isn't possible to {@linkplain #isCompatibleWith change} the protected status of a
+ * VM instance; protected and non-protected VMs always have different secrets.
+ *
+ * @see VirtualMachineManager#getCapabilities
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public Builder setProtectedVm(boolean protectedVm) {
+ if (protectedVm) {
+ if (!HypervisorProperties.hypervisor_protected_vm_supported().orElse(false)) {
+ throw new UnsupportedOperationException(
+ "Protected VMs are not supported on this device.");
+ }
+ } else {
+ if (!HypervisorProperties.hypervisor_vm_supported().orElse(false)) {
+ throw new UnsupportedOperationException(
+ "Non-protected VMs are not supported on this device.");
+ }
+ }
+ mProtectedVm = protectedVm;
+ mProtectedVmSet = true;
+ return this;
+ }
+
+ /**
+ * Sets the amount of RAM to give the VM, in bytes. If not explicitly set then a default
+ * size will be used.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public Builder setMemoryBytes(@IntRange(from = 1) long memoryBytes) {
+ if (memoryBytes <= 0) {
+ throw new IllegalArgumentException("Memory size must be positive");
+ }
+ mMemoryBytes = memoryBytes;
+ return this;
+ }
+
+ /**
+ * Sets the CPU topology configuration of the VM. Defaults to {@link #CPU_TOPOLOGY_ONE_CPU}.
+ *
+ * <p>This determines how many virtual CPUs will be created, and their performance and
+ * scheduling characteristics, such as affinity masks. Topology also has an effect on memory
+ * usage as each vCPU requires additional memory to keep its state.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public Builder setCpuTopology(@CpuTopology int cpuTopology) {
+ if (cpuTopology != CPU_TOPOLOGY_ONE_CPU && cpuTopology != CPU_TOPOLOGY_MATCH_HOST) {
+ throw new IllegalArgumentException("Invalid cpuTopology: " + cpuTopology);
+ }
+ mCpuTopology = cpuTopology;
+ return this;
+ }
+
+ /**
+ * Sets the size (in bytes) of encrypted storage available to the VM. If not set, no
+ * encrypted storage is provided.
+ *
+ * <p>The storage is encrypted with a key deterministically derived from the VM identity
+ *
+ * <p>The encrypted storage is persistent across VM reboots as well as device reboots. The
+ * backing file (containing encrypted data) is stored in the app's private data directory.
+ *
+ * <p>Note - There is no integrity guarantee or rollback protection on the storage in case
+ * the encrypted data is modified.
+ *
+ * <p>Deleting the VM will delete the encrypted data - there is no way to recover that data.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public Builder setEncryptedStorageBytes(@IntRange(from = 1) long encryptedStorageBytes) {
+ if (encryptedStorageBytes <= 0) {
+ throw new IllegalArgumentException("Encrypted Storage size must be positive");
+ }
+ mEncryptedStorageBytes = encryptedStorageBytes;
+ return this;
+ }
+
+ /**
+ * Sets whether to allow the app to read the VM outputs (console / log). Default is {@code
+ * false}.
+ *
+ * <p>By default, console and log outputs of a {@linkplain #setDebugLevel debuggable} VM are
+ * automatically forwarded to the host logcat. Setting this as {@code true} will allow the
+ * app to directly read {@linkplain VirtualMachine#getConsoleOutput console output} and
+ * {@linkplain VirtualMachine#getLogOutput log output}, instead of forwarding them to the
+ * host logcat.
+ *
+ * <p>If you turn on output capture, you must consume data from {@link
+ * VirtualMachine#getConsoleOutput} and {@link VirtualMachine#getLogOutput} - because
+ * otherwise the code in the VM may get blocked when the pipe buffer fills up.
+ *
+ * <p>The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be
+ * set as true.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public Builder setVmOutputCaptured(boolean captured) {
+ mVmOutputCaptured = captured;
+ return this;
+ }
+
+ /**
+ * Sets whether to allow the app to write to the VM console. Default is {@code false}.
+ *
+ * <p>Setting this as {@code true} will allow the app to directly write into {@linkplain
+ * VirtualMachine#getConsoleInput console input}.
+ *
+ * <p>The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be
+ * set as true.
+ *
+ * @hide
+ */
+ @TestApi
+ @NonNull
+ public Builder setVmConsoleInputSupported(boolean supported) {
+ mVmConsoleInputSupported = supported;
+ return this;
+ }
+
+ /**
+ * Sets the path to the disk image with vendor-specific modules.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+ @NonNull
+ public Builder setVendorDiskImage(@NonNull File vendorDiskImage) {
+ mVendorDiskImage =
+ requireNonNull(vendorDiskImage, "vendor disk image must not be null");
+ return this;
+ }
+
+ /**
+ * Sets an OS for the VM. Defaults to {@code "microdroid"}.
+ *
+ * <p>See {@link VirtualMachineManager#getSupportedOSList} for available OS names.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION)
+ @NonNull
+ public Builder setOs(@NonNull String os) {
+ mOs = requireNonNull(os, "os must not be null");
+ return this;
+ }
+ }
+}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java b/java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java
new file mode 100644
index 0000000..710925d
--- /dev/null
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.virtualmachine;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+
+import java.io.IOException;
+
+/**
+ * A VM descriptor that captures the state of a Virtual Machine.
+ *
+ * <p>You can capture the current state of VM by creating an instance of this class with {@link
+ * VirtualMachine#toDescriptor}, optionally pass it to another App, and then build an identical VM
+ * with the descriptor received.
+ *
+ * @hide
+ */
+@SystemApi
+public final class VirtualMachineDescriptor implements Parcelable, AutoCloseable {
+ private volatile boolean mClosed = false;
+ @NonNull private final ParcelFileDescriptor mConfigFd;
+ @NonNull private final ParcelFileDescriptor mInstanceImgFd;
+ // File descriptor of the image backing the encrypted storage - Will be null if encrypted
+ // storage is not enabled. */
+ @Nullable private final ParcelFileDescriptor mEncryptedStoreFd;
+
+ @Override
+ public int describeContents() {
+ return CONTENTS_FILE_DESCRIPTOR;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ checkNotClosed();
+ out.writeParcelable(mConfigFd, flags);
+ out.writeParcelable(mInstanceImgFd, flags);
+ out.writeParcelable(mEncryptedStoreFd, flags);
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<VirtualMachineDescriptor> CREATOR =
+ new Parcelable.Creator<>() {
+ public VirtualMachineDescriptor createFromParcel(Parcel in) {
+ return new VirtualMachineDescriptor(in);
+ }
+
+ public VirtualMachineDescriptor[] newArray(int size) {
+ return new VirtualMachineDescriptor[size];
+ }
+ };
+
+ /**
+ * @return File descriptor of the VM configuration file config.xml.
+ */
+ @NonNull
+ ParcelFileDescriptor getConfigFd() {
+ checkNotClosed();
+ return mConfigFd;
+ }
+
+ /**
+ * @return File descriptor of the instance.img of the VM.
+ */
+ @NonNull
+ ParcelFileDescriptor getInstanceImgFd() {
+ checkNotClosed();
+ return mInstanceImgFd;
+ }
+
+ /**
+ * @return File descriptor of image backing the encrypted storage.
+ * <p>This method will return null if encrypted storage is not enabled.
+ */
+ @Nullable
+ ParcelFileDescriptor getEncryptedStoreFd() {
+ checkNotClosed();
+ return mEncryptedStoreFd;
+ }
+
+ VirtualMachineDescriptor(
+ @NonNull ParcelFileDescriptor configFd,
+ @NonNull ParcelFileDescriptor instanceImgFd,
+ @Nullable ParcelFileDescriptor encryptedStoreFd) {
+ mConfigFd = requireNonNull(configFd);
+ mInstanceImgFd = requireNonNull(instanceImgFd);
+ mEncryptedStoreFd = encryptedStoreFd;
+ }
+
+ private VirtualMachineDescriptor(Parcel in) {
+ mConfigFd = requireNonNull(readParcelFileDescriptor(in));
+ mInstanceImgFd = requireNonNull(readParcelFileDescriptor(in));
+ mEncryptedStoreFd = readParcelFileDescriptor(in);
+ }
+
+ private ParcelFileDescriptor readParcelFileDescriptor(Parcel in) {
+ return in.readParcelable(
+ ParcelFileDescriptor.class.getClassLoader(), ParcelFileDescriptor.class);
+ }
+
+ /**
+ * Release any resources held by this descriptor. Calling {@code close} on an already-closed
+ * descriptor has no effect.
+ */
+ @Override
+ public void close() {
+ mClosed = true;
+ // Let the compiler do the work: close everything, throw if any of them fail, skipping null.
+ try (mConfigFd;
+ mInstanceImgFd;
+ mEncryptedStoreFd) {
+ } catch (IOException ignored) {
+ // PFD already swallows exceptions from closing the fd. There's no reason to propagate
+ // this to the caller.
+ }
+ }
+
+ private void checkNotClosed() {
+ if (mClosed) {
+ throw new IllegalStateException("Descriptor has been closed");
+ }
+ }
+}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineException.java b/java/framework/src/android/system/virtualmachine/VirtualMachineException.java
new file mode 100644
index 0000000..9948fda
--- /dev/null
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineException.java
@@ -0,0 +1,40 @@
+/*
+ * 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.annotation.Nullable;
+import android.annotation.SystemApi;
+
+/**
+ * Exception thrown when operations on virtual machines fail.
+ *
+ * @hide
+ */
+@SystemApi
+public class VirtualMachineException extends Exception {
+ VirtualMachineException(@Nullable String message) {
+ super(message);
+ }
+
+ VirtualMachineException(@Nullable String message, @Nullable Throwable cause) {
+ super(message, cause);
+ }
+
+ VirtualMachineException(@Nullable Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
new file mode 100644
index 0000000..5020ff0
--- /dev/null
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -0,0 +1,396 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
+import android.annotation.RequiresPermission;
+import android.annotation.StringDef;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.sysprop.HypervisorProperties;
+import android.system.virtualizationservice.IVirtualizationService;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.system.virtualmachine.flags.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Manages {@linkplain VirtualMachine virtual machine} instances created by an app. Each instance is
+ * created from a {@linkplain VirtualMachineConfig configuration} that defines the shape of the VM
+ * (RAM, CPUs), the code to execute within it, etc.
+ *
+ * <p>Each virtual machine instance is named; the configuration and related state of each is
+ * persisted in the app's private data directory and an instance can be retrieved given the name.
+ * The name must be a valid directory name and must not contain '/'.
+ *
+ * <p>The app can then start, stop and otherwise interact with the VM.
+ *
+ * <p>An instance of {@link VirtualMachineManager} can be obtained by calling {@link
+ * Context#getSystemService(Class)}.
+ *
+ * @hide
+ */
+@SystemApi
+@RequiresFeature(PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK)
+public class VirtualMachineManager {
+ /**
+ * A lock used to synchronize the creation of virtual machines. It protects {@link #mVmsByName},
+ * but is also held throughout VM creation / retrieval / deletion, to prevent these actions
+ * racing with each other.
+ */
+ private static final Object sCreateLock = new Object();
+
+ @NonNull private final Context mContext;
+
+ /** @hide */
+ public VirtualMachineManager(@NonNull Context context) {
+ mContext = requireNonNull(context);
+ }
+
+ @GuardedBy("sCreateLock")
+ private final Map<String, WeakReference<VirtualMachine>> mVmsByName = new ArrayMap<>();
+
+ /**
+ * Capabilities of the virtual machine implementation.
+ *
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "CAPABILITY_", flag = true, value = {
+ CAPABILITY_PROTECTED_VM,
+ CAPABILITY_NON_PROTECTED_VM
+ })
+ public @interface Capability {}
+
+ /**
+ * The implementation supports creating protected VMs, whose memory is inaccessible to the host
+ * OS.
+ *
+ * @see VirtualMachineConfig.Builder#setProtectedVm
+ */
+ public static final int CAPABILITY_PROTECTED_VM = 1;
+
+ /**
+ * The implementation supports creating non-protected VMs, whose memory is accessible to the
+ * host OS.
+ *
+ * @see VirtualMachineConfig.Builder#setProtectedVm
+ */
+ public static final int CAPABILITY_NON_PROTECTED_VM = 2;
+
+ /**
+ * Features provided by {@link VirtualMachineManager}.
+ *
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(
+ prefix = "FEATURE_",
+ value = {
+ FEATURE_DICE_CHANGES,
+ FEATURE_LLPVM_CHANGES,
+ FEATURE_MULTI_TENANT,
+ FEATURE_REMOTE_ATTESTATION,
+ FEATURE_VENDOR_MODULES,
+ })
+ public @interface Features {}
+
+ /**
+ * Feature to include new data in the VM DICE chain.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ public static final String FEATURE_DICE_CHANGES = IVirtualizationService.FEATURE_DICE_CHANGES;
+
+ /**
+ * Feature to run payload as non-root user.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ public static final String FEATURE_MULTI_TENANT = IVirtualizationService.FEATURE_MULTI_TENANT;
+
+ /**
+ * Feature to allow remote attestation in Microdroid.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ public static final String FEATURE_REMOTE_ATTESTATION =
+ IVirtualizationService.FEATURE_REMOTE_ATTESTATION;
+
+ /**
+ * Feature to allow vendor modules in Microdroid.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ public static final String FEATURE_VENDOR_MODULES =
+ IVirtualizationService.FEATURE_VENDOR_MODULES;
+
+ /**
+ * Feature to enable Secretkeeper protected secrets in Microdroid based pVMs.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ public static final String FEATURE_LLPVM_CHANGES = IVirtualizationService.FEATURE_LLPVM_CHANGES;
+
+ /**
+ * Returns a set of flags indicating what this implementation of virtualization is capable of.
+ *
+ * @see #CAPABILITY_PROTECTED_VM
+ * @see #CAPABILITY_NON_PROTECTED_VM
+ * @hide
+ */
+ @SystemApi
+ @Capability
+ public int getCapabilities() {
+ @Capability int result = 0;
+ if (HypervisorProperties.hypervisor_protected_vm_supported().orElse(false)) {
+ result |= CAPABILITY_PROTECTED_VM;
+ }
+ if (HypervisorProperties.hypervisor_vm_supported().orElse(false)) {
+ result |= CAPABILITY_NON_PROTECTED_VM;
+ }
+ return result;
+ }
+
+ /**
+ * 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.
+ *
+ * <p>Each successful call to this method creates a new (and different) virtual machine even if
+ * the name and the config are the same as a deleted one. The new virtual machine will initially
+ * be stopped.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @throws VirtualMachineException if the VM cannot be created, or there is an existing VM with
+ * the given name.
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ @WorkerThread
+ @RequiresPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION)
+ public VirtualMachine create(@NonNull String name, @NonNull VirtualMachineConfig config)
+ throws VirtualMachineException {
+ synchronized (sCreateLock) {
+ return createLocked(name, config);
+ }
+ }
+
+ @NonNull
+ @GuardedBy("sCreateLock")
+ private VirtualMachine createLocked(@NonNull String name, @NonNull VirtualMachineConfig config)
+ throws VirtualMachineException {
+ VirtualMachine vm = VirtualMachine.create(mContext, name, config);
+ mVmsByName.put(name, new WeakReference<>(vm));
+ return vm;
+ }
+
+ /**
+ * Returns an existing {@link VirtualMachine} with the given name. Returns null if there is no
+ * such virtual machine.
+ *
+ * <p>There is at most one {@code VirtualMachine} object corresponding to a given virtual
+ * machine instance. Multiple calls to get() passing the same name will get the same object
+ * returned, until the virtual machine is deleted (via {@link #delete}) and then recreated.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @see #getOrCreate
+ * @throws VirtualMachineException if the virtual machine exists but could not be successfully
+ * retrieved. This can be resolved by calling {@link #delete} on the VM.
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ @Nullable
+ public VirtualMachine get(@NonNull String name) throws VirtualMachineException {
+ synchronized (sCreateLock) {
+ return getLocked(name);
+ }
+ }
+
+ @Nullable
+ @GuardedBy("sCreateLock")
+ private VirtualMachine getLocked(@NonNull String name) throws VirtualMachineException {
+ VirtualMachine vm = getVmByName(name);
+ if (vm != null) return vm;
+
+ vm = VirtualMachine.load(mContext, name);
+ if (vm != null) {
+ mVmsByName.put(name, new WeakReference<>(vm));
+ }
+ return vm;
+ }
+
+ /**
+ * Imports a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
+ * with the given name.
+ *
+ * <p>The new virtual machine will be in the same state as the descriptor indicates. The
+ * descriptor is automatically closed and cannot be used again.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @throws VirtualMachineException if the VM cannot be imported or the {@code
+ * VirtualMachineDescriptor} has already been closed.
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ @WorkerThread
+ public VirtualMachine importFromDescriptor(
+ @NonNull String name, @NonNull VirtualMachineDescriptor vmDescriptor)
+ throws VirtualMachineException {
+ synchronized (sCreateLock) {
+ VirtualMachine vm = VirtualMachine.fromDescriptor(mContext, name, vmDescriptor);
+ mVmsByName.put(name, new WeakReference<>(vm));
+ return vm;
+ }
+ }
+
+ /**
+ * Returns an existing {@link VirtualMachine} if it exists, or create a new one. The config
+ * parameter is used only when a new virtual machine is created.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @throws VirtualMachineException if the virtual machine could not be created or retrieved.
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ @NonNull
+ public VirtualMachine getOrCreate(@NonNull String name, @NonNull VirtualMachineConfig config)
+ throws VirtualMachineException {
+ synchronized (sCreateLock) {
+ VirtualMachine vm = getLocked(name);
+ if (vm != null) {
+ return vm;
+ } else {
+ return createLocked(name, config);
+ }
+ }
+ }
+
+ /**
+ * Deletes an existing {@link VirtualMachine}. Deleting a virtual machine means deleting any
+ * persisted data associated with it including the per-VM secret. This is an irreversible
+ * action. A virtual machine once deleted can never be restored. A new virtual machine created
+ * with the same name is different from an already deleted virtual machine even if it has the
+ * same config.
+ *
+ * <p>NOTE: This method may block and should not be called on the main thread.
+ *
+ * @throws VirtualMachineException if the virtual machine does not exist, is not stopped, or
+ * cannot be deleted.
+ * @hide
+ */
+ @SystemApi
+ @WorkerThread
+ public void delete(@NonNull String name) throws VirtualMachineException {
+ synchronized (sCreateLock) {
+ VirtualMachine vm = getVmByName(name);
+ if (vm == null) {
+ VirtualMachine.deleteVmDirectory(mContext, name);
+ } else {
+ vm.delete(mContext, name);
+ }
+ mVmsByName.remove(name);
+ }
+ }
+
+ @Nullable
+ @GuardedBy("sCreateLock")
+ private VirtualMachine getVmByName(@NonNull String name) {
+ requireNonNull(name);
+ WeakReference<VirtualMachine> weakReference = mVmsByName.get(name);
+ if (weakReference != null) {
+ VirtualMachine vm = weakReference.get();
+ if (vm != null && vm.getStatus() != VirtualMachine.STATUS_DELETED) {
+ return vm;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a list of supported OS names.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ @NonNull
+ public List<String> getSupportedOSList() throws VirtualMachineException {
+ synchronized (sCreateLock) {
+ VirtualizationService service = VirtualizationService.getInstance();
+ try {
+ return Arrays.asList(service.getBinder().getSupportedOSList());
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+ }
+
+ /**
+ * Returns {@code true} if given {@code featureName} is enabled.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
+ @RequiresPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION)
+ public boolean isFeatureEnabled(@Features String featureName) throws VirtualMachineException {
+ synchronized (sCreateLock) {
+ VirtualizationService service = VirtualizationService.getInstance();
+ try {
+ return service.getBinder().isFeatureEnabled(featureName);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+ }
+}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java b/java/framework/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java
new file mode 100644
index 0000000..30ac425
--- /dev/null
+++ b/java/framework/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.virtualmachine;
+
+import static android.content.pm.PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK;
+
+import android.annotation.SystemApi;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+
+/**
+ * Holds initialization code for virtualization module
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public class VirtualizationFrameworkInitializer {
+
+ private VirtualizationFrameworkInitializer() {}
+
+ /**
+ * Called by the static initializer in the {@link SystemServiceRegistry}, and registers {@link
+ * VirtualMachineManager} to the {@link Context}. so that it's accessible from {@link
+ * Context#getSystemService(String)}.
+ */
+ public static void registerServiceWrappers() {
+ // Note: it's important that the getPackageManager().hasSystemFeature() check is executed
+ // in the lambda, and not directly in the registerServiceWrappers method. The
+ // registerServiceWrappers is called during Zygote static initialization, and at that
+ // point the PackageManager is not available yet.
+ //
+ // On the other hand, the lambda is executed after the app calls Context.getSystemService
+ // (VirtualMachineManager.class), at which point the PackageManager is available. The
+ // result of the lambda is cached on per-context basis.
+ SystemServiceRegistry.registerContextAwareService(
+ Context.VIRTUALIZATION_SERVICE,
+ VirtualMachineManager.class,
+ ctx ->
+ ctx.getPackageManager().hasSystemFeature(FEATURE_VIRTUALIZATION_FRAMEWORK)
+ ? new VirtualMachineManager(ctx)
+ : null);
+ }
+}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualizationService.java b/java/framework/src/android/system/virtualmachine/VirtualizationService.java
new file mode 100644
index 0000000..57990a9
--- /dev/null
+++ b/java/framework/src/android/system/virtualmachine/VirtualizationService.java
@@ -0,0 +1,99 @@
+/*
+ * 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.annotation.NonNull;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.system.virtualizationservice.IVirtualizationService;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.lang.ref.WeakReference;
+
+/** A running instance of virtmgr that is hosting a VirtualizationService AIDL service. */
+class VirtualizationService {
+ static {
+ System.loadLibrary("virtualizationservice_jni");
+ }
+
+ /* Soft reference caching the last created instance of this class. */
+ @GuardedBy("VirtualMachineManager.sCreateLock")
+ private static WeakReference<VirtualizationService> sInstance;
+
+ /*
+ * Client FD for UDS connection to virtmgr's RpcBinder server. Closing it
+ * will make virtmgr shut down.
+ */
+ private final ParcelFileDescriptor mClientFd;
+
+ /* Persistent connection to IVirtualizationService. */
+ private final IVirtualizationService mBinder;
+
+ private static native int nativeSpawn();
+
+ private native IBinder nativeConnect(int clientFd);
+
+ private native boolean nativeIsOk(int clientFd);
+
+ /*
+ * Spawns a new virtmgr subprocess that will host a VirtualizationService
+ * AIDL service.
+ */
+ private VirtualizationService() throws VirtualMachineException {
+ int clientFd = nativeSpawn();
+ if (clientFd < 0) {
+ throw new VirtualMachineException("Could not spawn Virtualization Manager");
+ }
+ mClientFd = ParcelFileDescriptor.adoptFd(clientFd);
+
+ IBinder binder = nativeConnect(mClientFd.getFd());
+ if (binder == null) {
+ throw new VirtualMachineException("Could not connect to Virtualization Manager");
+ }
+ mBinder = IVirtualizationService.Stub.asInterface(binder);
+ }
+
+ /* Returns the IVirtualizationService binder. */
+ @NonNull
+ IVirtualizationService getBinder() {
+ return mBinder;
+ }
+
+ /*
+ * Checks the state of the client FD. Returns false if the FD is in erroneous state
+ * or if the other endpoint had closed its FD.
+ */
+ private boolean isOk() {
+ return nativeIsOk(mClientFd.getFd());
+ }
+
+ /*
+ * Returns an instance of this class. Might spawn a new instance if one doesn't exist, or
+ * if the previous instance had crashed.
+ */
+ @GuardedBy("VirtualMachineManager.sCreateLock")
+ @NonNull
+ static VirtualizationService getInstance() throws VirtualMachineException {
+ VirtualizationService service = (sInstance == null) ? null : sInstance.get();
+ if (service == null || !service.isOk()) {
+ service = new VirtualizationService();
+ sInstance = new WeakReference<>(service);
+ }
+ return service;
+ }
+}
diff --git a/java/jni/Android.bp b/java/jni/Android.bp
new file mode 100644
index 0000000..74a1766
--- /dev/null
+++ b/java/jni/Android.bp
@@ -0,0 +1,36 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_shared {
+ name: "libvirtualizationservice_jni",
+ defaults: ["avf_build_flags_cc"],
+ srcs: [
+ "android_system_virtualmachine_VirtualizationService.cpp",
+ ],
+ apex_available: ["com.android.virt"],
+ shared_libs: [
+ "libbase",
+ "libbinder_ndk",
+ "libbinder_rpc_unstable",
+ "liblog",
+ "libnativehelper",
+ ],
+}
+
+cc_library_shared {
+ name: "libvirtualmachine_jni",
+ defaults: ["avf_build_flags_cc"],
+ srcs: [
+ "android_system_virtualmachine_VirtualMachine.cpp",
+ ],
+ apex_available: ["com.android.virt"],
+ shared_libs: [
+ "android.system.virtualizationservice-ndk",
+ "libbase",
+ "libbinder_ndk",
+ "libbinder_rpc_unstable",
+ "liblog",
+ "libnativehelper",
+ ],
+}
diff --git a/java/jni/android_system_virtualmachine_VirtualMachine.cpp b/java/jni/android_system_virtualmachine_VirtualMachine.cpp
new file mode 100644
index 0000000..b3354cc
--- /dev/null
+++ b/java/jni/android_system_virtualmachine_VirtualMachine.cpp
@@ -0,0 +1,67 @@
+/*
+ * Copyright 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.
+ */
+
+#define LOG_TAG "VirtualMachine"
+
+#include <aidl/android/system/virtualizationservice/IVirtualMachine.h>
+#include <android/binder_auto_utils.h>
+#include <android/binder_ibinder_jni.h>
+#include <jni.h>
+#include <log/log.h>
+
+#include <binder_rpc_unstable.hpp>
+#include <tuple>
+
+#include "common.h"
+
+extern "C" JNIEXPORT jobject JNICALL
+Java_android_system_virtualmachine_VirtualMachine_nativeConnectToVsockServer(
+ JNIEnv* env, [[maybe_unused]] jclass clazz, jobject vmBinder, jint port) {
+ using aidl::android::system::virtualizationservice::IVirtualMachine;
+ using ndk::ScopedFileDescriptor;
+ using ndk::SpAIBinder;
+
+ auto vm = IVirtualMachine::fromBinder(SpAIBinder{AIBinder_fromJavaBinder(env, vmBinder)});
+
+ std::tuple args{env, vm.get(), port};
+ using Args = decltype(args);
+
+ auto requestFunc = [](void* param) {
+ auto [env, vm, port] = *static_cast<Args*>(param);
+
+ ScopedFileDescriptor fd;
+ if (auto status = vm->connectVsock(port, &fd); !status.isOk()) {
+ env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
+ ("Failed to connect vsock: " + status.getDescription()).c_str());
+ return -1;
+ }
+
+ // take ownership
+ int ret = fd.get();
+ *fd.getR() = -1;
+
+ return ret;
+ };
+
+ RpcSessionHandle session;
+ // We need a thread pool to be able to support linkToDeath, or callbacks
+ // (b/268335700). These threads are currently created eagerly, so we don't
+ // want too many. The number 1 is chosen after some discussion, and to match
+ // the server-side default (mMaxThreads on RpcServer).
+ ARpcSession_setMaxIncomingThreads(session.get(), 1);
+ auto client = ARpcSession_setupPreconnectedClient(session.get(), requestFunc, &args);
+ return AIBinder_toJavaBinder(env, client);
+}
diff --git a/java/jni/android_system_virtualmachine_VirtualizationService.cpp b/java/jni/android_system_virtualmachine_VirtualizationService.cpp
new file mode 100644
index 0000000..fbd1fd5
--- /dev/null
+++ b/java/jni/android_system_virtualmachine_VirtualizationService.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "VirtualizationService"
+
+#include <android-base/unique_fd.h>
+#include <android/binder_ibinder_jni.h>
+#include <jni.h>
+#include <log/log.h>
+#include <poll.h>
+
+#include <string>
+
+#include "common.h"
+
+using namespace android::base;
+
+static constexpr const char VIRTMGR_PATH[] = "/apex/com.android.virt/bin/virtmgr";
+static constexpr size_t VIRTMGR_THREADS = 2;
+
+extern "C" JNIEXPORT jint JNICALL
+Java_android_system_virtualmachine_VirtualizationService_nativeSpawn(
+ JNIEnv* env, [[maybe_unused]] jclass clazz) {
+ unique_fd serverFd, clientFd;
+ if (!Socketpair(SOCK_STREAM, &serverFd, &clientFd)) {
+ env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
+ ("Failed to create socketpair: " + std::string(strerror(errno))).c_str());
+ return -1;
+ }
+
+ unique_fd waitFd, readyFd;
+ if (!Pipe(&waitFd, &readyFd, 0)) {
+ env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
+ ("Failed to create pipe: " + std::string(strerror(errno))).c_str());
+ return -1;
+ }
+
+ if (fork() == 0) {
+ // Close client's FDs.
+ clientFd.reset();
+ waitFd.reset();
+
+ auto strServerFd = std::to_string(serverFd.get());
+ auto strReadyFd = std::to_string(readyFd.get());
+
+ execl(VIRTMGR_PATH, VIRTMGR_PATH, "--rpc-server-fd", strServerFd.c_str(), "--ready-fd",
+ strReadyFd.c_str(), NULL);
+ }
+
+ // Close virtmgr's FDs.
+ serverFd.reset();
+ readyFd.reset();
+
+ // Wait for the server to signal its readiness by closing its end of the pipe.
+ char buf;
+ if (read(waitFd.get(), &buf, sizeof(buf)) < 0) {
+ env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
+ "Failed to wait for VirtualizationService to be ready");
+ return -1;
+ }
+
+ return clientFd.release();
+}
+
+extern "C" JNIEXPORT jobject JNICALL
+Java_android_system_virtualmachine_VirtualizationService_nativeConnect(JNIEnv* env,
+ [[maybe_unused]] jobject obj,
+ int clientFd) {
+ RpcSessionHandle session;
+ ARpcSession_setFileDescriptorTransportMode(session.get(),
+ ARpcSession_FileDescriptorTransportMode::Unix);
+ ARpcSession_setMaxIncomingThreads(session.get(), VIRTMGR_THREADS);
+ // SAFETY - ARpcSession_setupUnixDomainBootstrapClient does not take ownership of clientFd.
+ auto client = ARpcSession_setupUnixDomainBootstrapClient(session.get(), clientFd);
+ return AIBinder_toJavaBinder(env, client);
+}
+
+extern "C" JNIEXPORT jboolean JNICALL
+Java_android_system_virtualmachine_VirtualizationService_nativeIsOk(JNIEnv* env,
+ [[maybe_unused]] jobject obj,
+ int clientFd) {
+ /* Setting events=0 only returns POLLERR, POLLHUP or POLLNVAL. */
+ struct pollfd pfds[] = {{.fd = clientFd, .events = 0}};
+ if (poll(pfds, /*nfds*/ 1, /*timeout*/ 0) < 0) {
+ env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
+ ("Failed to poll client FD: " + std::string(strerror(errno))).c_str());
+ return false;
+ }
+ return pfds[0].revents == 0;
+}
diff --git a/java/jni/common.h b/java/jni/common.h
new file mode 100644
index 0000000..c70ba76
--- /dev/null
+++ b/java/jni/common.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <binder_rpc_unstable.hpp>
+
+// Wrapper around ARpcSession handle that automatically frees the handle when
+// it goes out of scope.
+class RpcSessionHandle {
+public:
+ RpcSessionHandle() : mHandle(ARpcSession_new()) {}
+ ~RpcSessionHandle() { ARpcSession_free(mHandle); }
+
+ ARpcSession* get() { return mHandle; }
+
+private:
+ ARpcSession* mHandle;
+};
diff --git a/java/service/Android.bp b/java/service/Android.bp
new file mode 100644
index 0000000..9c1fa01
--- /dev/null
+++ b/java/service/Android.bp
@@ -0,0 +1,30 @@
+// Copyright (C) 2024 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "service-virtualization",
+ srcs: [
+ "src/**/*.java",
+ ],
+ defaults: [
+ "framework-system-server-module-defaults",
+ ],
+ sdk_version: "system_server_current",
+ apex_available: ["com.android.virt"],
+ installable: true,
+}
diff --git a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
new file mode 100644
index 0000000..2905acd
--- /dev/null
+++ b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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.system.virtualmachine;
+
+import android.content.Context;
+import com.android.server.SystemService;
+
+/** TODO */
+public class VirtualizationSystemService extends SystemService {
+
+ public VirtualizationSystemService(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {}
+}