| # Android Virtualization Framework API | |
| These Java APIs allow an app to configure and run a Virtual Machine running | |
| [Microdroid](../../build/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](../libvm_payload/README.md) | |
| The API classes are all in the | |
| [`android.system.virtualmachine`](src/android/system/virtualmachine) package. | |
| All of these APIs were introduced in API level 34 (Android 14). The classes may | |
| not exist in devices running an earlier version. | |
| Note that they are all `@SystemApi` and require the restricted | |
| `android.permission.MANAGE_VIRTUAL_MACHINE` permission, so they are not | |
| available to third party apps. In Android 14 the permission was available only to | |
| privileged apps; in Android 15 it is available to all preinstalled apps. On both | |
| versions it can also be granted to other apps via `adb shell pm grant` for | |
| development purposes. | |
| ## 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()`](../libs/libvm_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()`](../libs/libvm_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()`](../libs/libvm_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()`](../libs/libvm_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()`](../libs/libvm_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. | |
| For more info see [README](https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/libs/framework-virtualization/README.md) | |
| # 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`. | |