blob: a742cccec4b43e3df31477205196991721a70b24 [file] [log] [blame] [view]
# 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`.