script-astra/Android/Sdk/sources/android-35/android/system/virtualmachine/VirtualMachine.java

1719 lines
68 KiB
Java
Raw Permalink Normal View History

2025-01-20 15:15:20 +00:00
/*
* 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.view.KeyEvent;
import android.view.MotionEvent;
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 com.android.internal.annotations.GuardedBy;
import com.android.system.virtualmachine.flags.Flags;
import libcore.io.IoBridge;
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.ByteBuffer;
import java.nio.ByteOrder;
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.Arrays;
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;
private ParcelFileDescriptor mTouchSock;
private ParcelFileDescriptor mKeySock;
/**
* 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<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);
try {
mInstanceIdPath =
(mVirtualizationService
.getBinder()
.isFeatureEnabled(IVirtualizationService.FEATURE_LLPVM_CHANGES))
? new File(thisVmDir, INSTANCE_ID_FILE)
: null;
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
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());
}
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}.
*
* <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);
}
}
private android.system.virtualizationservice.VirtualMachineConfig
createVirtualMachineConfigForRawFrom(VirtualMachineConfig vmConfig)
throws IllegalStateException, IOException {
VirtualMachineRawConfig rawConfig = vmConfig.toVsRawConfig();
// Handle input devices here
List<InputDevice> inputDevices = new ArrayList<>();
if (vmConfig.getCustomImageConfig() != null
&& rawConfig.displayConfig != null) {
if (vmConfig.getCustomImageConfig().useTouch()) {
ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
mTouchSock = pfds[0];
InputDevice.SingleTouch t = new InputDevice.SingleTouch();
t.width = rawConfig.displayConfig.width;
t.height = rawConfig.displayConfig.height;
t.pfd = pfds[1];
inputDevices.add(InputDevice.singleTouch(t));
}
if (vmConfig.getCustomImageConfig().useKeyboard()) {
ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createSocketPair();
mKeySock = pfds[0];
InputDevice.Keyboard k = new InputDevice.Keyboard();
k.pfd = pfds[1];
inputDevices.add(InputDevice.keyboard(k));
}
}
rawConfig.inputDevices = inputDevices.toArray(new InputDevice[0]);
return android.system.virtualizationservice.VirtualMachineConfig.rawConfig(rawConfig);
}
private static record InputEvent(short type, short code, int value) {}
/** @hide */
public boolean sendKeyEvent(KeyEvent event) {
if (mKeySock == null) {
Log.d(TAG, "mKeySock == null");
return false;
}
// from include/uapi/linux/input-event-codes.h in the kernel.
short EV_SYN = 0x00;
short EV_KEY = 0x01;
short SYN_REPORT = 0x00;
boolean down = event.getAction() != MotionEvent.ACTION_UP;
return writeEventsToSock(
mKeySock,
Arrays.asList(
new InputEvent(EV_KEY, (short) event.getScanCode(), down ? 1 : 0),
new InputEvent(EV_SYN, SYN_REPORT, 0)));
}
/** @hide */
public boolean sendSingleTouchEvent(MotionEvent event) {
if (mTouchSock == null) {
Log.d(TAG, "mTouchSock == null");
return false;
}
// from include/uapi/linux/input-event-codes.h in the kernel.
short EV_SYN = 0x00;
short EV_ABS = 0x03;
short EV_KEY = 0x01;
short BTN_TOUCH = 0x14a;
short ABS_X = 0x00;
short ABS_Y = 0x01;
short SYN_REPORT = 0x00;
int x = (int) event.getX();
int y = (int) event.getY();
boolean down = event.getAction() != MotionEvent.ACTION_UP;
return writeEventsToSock(
mTouchSock,
Arrays.asList(
new InputEvent(EV_ABS, ABS_X, x),
new InputEvent(EV_ABS, ABS_Y, y),
new InputEvent(EV_KEY, BTN_TOUCH, down ? 1 : 0),
new InputEvent(EV_SYN, SYN_REPORT, 0)));
}
private boolean writeEventsToSock(ParcelFileDescriptor sock, List<InputEvent> evtList) {
ByteBuffer byteBuffer =
ByteBuffer.allocate(8 /* (type: u16 + code: u16 + value: i32) */ * evtList.size());
byteBuffer.clear();
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
for (InputEvent e : evtList) {
byteBuffer.putShort(e.type);
byteBuffer.putShort(e.code);
byteBuffer.putInt(e.value);
}
try {
IoBridge.write(
sock.getFileDescriptor(), byteBuffer.array(), 0, byteBuffer.array().length);
} catch (IOException e) {
Log.d(TAG, "cannot send event", e);
return false;
}
return true;
}
private android.system.virtualizationservice.VirtualMachineConfig
createVirtualMachineConfigForAppFrom(
VirtualMachineConfig vmConfig, IVirtualizationService service)
throws RemoteException, IOException, VirtualMachineException {
VirtualMachineAppConfig appConfig = vmConfig.toVsConfig(mContext.getPackageManager());
appConfig.instanceImage = ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
appConfig.name = mName;
if (mInstanceIdPath != null) {
appConfig.instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
} else {
// FEATURE_LLPVM_CHANGES is disabled, instance_id is not used.
appConfig.instanceId = new byte[64];
}
if (mEncryptedStoreFilePath != null) {
appConfig.encryptedStorageImage =
ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_WRITE);
}
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 {
createIdSigsAndUpdateConfig(service, appConfig);
} catch (FileNotFoundException e) {
throw new VirtualMachineException("Failed to generate APK signature", e);
}
return android.system.virtualizationservice.VirtualMachineConfig.appConfig(appConfig);
}
/**
* 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();
android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
vmConfig.getCustomImageConfig() != null
? createVirtualMachineConfigForRawFrom(vmConfig)
: createVirtualMachineConfigForAppFrom(vmConfig, service);
mVirtualMachine =
service.createVm(
vmConfigParcel, mConsoleOutWriter, mConsoleInReader, 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<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),
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<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 importInstanceIdFrom(@NonNull ParcelFileDescriptor instanceIdFd)
throws VirtualMachineException {
try (FileChannel idOutput = new FileOutputStream(mInstanceIdPath).getChannel();
FileChannel idInput = new AutoCloseInputStream(instanceIdFd).getChannel()) {
idOutput.transferFrom(idInput, /*position=*/ 0, idInput.size());
} catch (IOException e) {
throw new VirtualMachineException("failed to copy instance_id", e);
}
}
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;
}
}
}
}