/* * 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.CallbackExecutor; 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.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.ErrnoException; import android.system.OsConstants; 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.InputDevice; import android.system.virtualizationservice.MemoryTrimLevel; import android.system.virtualizationservice.PartitionType; import android.system.virtualizationservice.VirtualMachineAppConfig; import android.system.virtualizationservice.VirtualMachineRawConfig; import android.system.virtualizationservice.VirtualMachineState; import android.util.JsonReader; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import com.android.internal.annotations.GuardedBy; import com.android.system.virtualmachine.flags.Flags; import libcore.io.IoBridge; import libcore.io.IoUtils; import java.io.File; import java.io.FileDescriptor; 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.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; 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.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; 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}. * *
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}. * *
The payload code running inside the VM has access to a set of native APIs; see the README * file for details. * *
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;
private ParcelFileDescriptor mTouchSock;
private ParcelFileDescriptor mKeySock;
private ParcelFileDescriptor mMouseSock;
/**
* 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 file for a VM containing Id. */
private static final String INSTANCE_ID_FILE = "instance_id";
/** 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;
/** File that contains the Id. This is NULL iff FEATURE_LLPVM is disabled */
@Nullable private final File mInstanceIdPath;
/**
* Unmodifiable list of extra apks. Apks are specified by the vm config, and corresponding
* idsigs are to be generated.
*/
@NonNull private final List The new virtual machine will be in the same state as the descriptor indicates.
*
* 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());
}
if (vm.mInstanceIdPath != null) {
vm.importInstanceIdFrom(vmDescriptor.getInstanceIdFd());
vm.claimInstance();
}
}
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 (Exception 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();
if (vm.mInstanceIdPath != null) {
try (FileOutputStream stream = new FileOutputStream(vm.mInstanceIdPath)) {
byte[] id = service.allocateInstanceId();
stream.write(id);
} catch (FileNotFoundException e) {
throw new VirtualMachineException("instance_id file missing", e);
} catch (IOException e) {
throw new VirtualMachineException("failed to persist instance_id", e);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
} catch (ServiceSpecificException | IllegalArgumentException e) {
throw new VirtualMachineException("failed to create instance_id", e);
}
}
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 {
vmInstanceCleanup(context, name);
} catch (Exception 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.mInstanceIdPath != null && !vm.mInstanceIdPath.exists()) {
throw new VirtualMachineException("instance_id file missing");
}
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;
}
vmInstanceCleanup(context, name);
}
// Delete the full VM directory and notify VirtualizationService to remove this
// VM instance for housekeeping.
@GuardedBy("VirtualMachineManager.sCreateLock")
static void vmInstanceCleanup(Context context, String name) throws VirtualMachineException {
File vmDir = getVmDir(context, name);
notifyInstanceRemoval(vmDir, VirtualizationService.getInstance());
try {
deleteRecursively(vmDir);
} catch (IOException e) {
throw new VirtualMachineException(e);
}
}
private static void notifyInstanceRemoval(
File vmDirectory, @NonNull VirtualizationService service) {
File instanceIdFile = new File(vmDirectory, INSTANCE_ID_FILE);
try {
byte[] instanceId = Files.readAllBytes(instanceIdFile.toPath());
service.getBinder().removeVmInstance(instanceId);
} catch (Exception e) {
// Deliberately ignoring error in removing VM instance. This potentially leads to
// unaccounted instances in the VS' database. But, nothing much can be done by caller.
Log.w(TAG, "Failed to notify VS to remove the VM instance", e);
}
}
// Claim the instance. This notifies the global VS about the ownership of this
// instance_id for housekeeping purpose.
void claimInstance() throws VirtualMachineException {
if (mInstanceIdPath != null) {
IVirtualizationService service = mVirtualizationService.getBinder();
try {
byte[] instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
service.claimVmInstance(instanceId);
}
catch (IOException e) {
throw new VirtualMachineException("failed to read instance_id", e);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
}
@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}.
*
* 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.
*
* 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 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 (mConnectVmConsole) {
createPtyConsole();
}
if (mVmOutputCaptured) {
createVmOutputPipes();
}
if (mVmConsoleInputSupported) {
createVmInputPipes();
}
ParcelFileDescriptor consoleOutFd = null;
if (mConnectVmConsole && mVmOutputCaptured) {
// If we are enabling output pipes AND the host console, then we tee the console
// output to both.
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
mTeeConsoleOutReader = pipe[0];
mTeeConsoleOutWriter = pipe[1];
consoleOutFd = mTeeConsoleOutWriter;
TeeWorker tee =
new TeeWorker(
mName + " console",
new FileInputStream(mTeeConsoleOutReader.getFileDescriptor()),
List.of(
new FileOutputStream(mPtyFd.getFileDescriptor()),
new FileOutputStream(
mConsoleOutWriter.getFileDescriptor())));
// If the VM is stopped then the tee worker thread would get an EOF or read()
// error which would tear down itself.
mConsoleExecutor.execute(tee);
} else if (mConnectVmConsole) {
consoleOutFd = mPtyFd;
} else if (mVmOutputCaptured) {
consoleOutFd = mConsoleOutWriter;
}
ParcelFileDescriptor consoleInFd = null;
if (mConnectVmConsole) {
consoleInFd = mPtyFd;
} else if (mVmConsoleInputSupported) {
consoleInFd = mConsoleInReader;
}
VirtualMachineConfig vmConfig = getConfig();
android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
vmConfig.getCustomImageConfig() != null
? createVirtualMachineConfigForRawFrom(vmConfig)
: createVirtualMachineConfigForAppFrom(vmConfig, service);
mVirtualMachine =
service.createVm(vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter);
mVirtualMachine.registerCallback(new CallbackTranslator(service));
mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
mVirtualMachine.start();
} catch (IOException e) {
throw new VirtualMachineException("failed to persist files", e);
} catch (IllegalStateException | ServiceSpecificException e) {
throw new VirtualMachineException(e);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
}
private void createIdSigsAndUpdateConfig(
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);
List 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.
*
* 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}.
*
* 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}.
*
* 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.
*
* 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.
*
* 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.
*
* A stopped virtual machine can be re-started by calling {@link #run()}.
*
* 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.
*
* 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.)
*
* The new config must be {@linkplain VirtualMachineConfig#isCompatibleWith compatible with}
* the existing config.
*
* 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.
*
* 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.
*
* The caller is responsible for closing the returned {@code ParcelFileDescriptor}.
*
* 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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),
mInstanceIdPath != null
? ParcelFileDescriptor.open(mInstanceIdPath, MODE_READ_ONLY)
: null,
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 { "extra_apks": [ { "path": "/system/app/foo.apk", }, ... ], ... }
*/
try {
List Supports non-blocking writes to the output streams by ignoring EAGAIN error.
*/
private static class TeeWorker implements Runnable {
private final String mName;
private final InputStream mIn;
private final List