| /* |
| * 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; |
| } |
| } |
| } |
| } |