/* * 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 java.util.Objects.requireNonNull; import android.Manifest; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.StringDef; import android.annotation.SystemApi; import android.annotation.TestApi; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.sysprop.HypervisorProperties; import android.system.virtualizationservice.DiskImage; import android.system.virtualizationservice.Partition; import android.system.virtualizationservice.VirtualMachineAppConfig; import android.system.virtualizationservice.VirtualMachinePayloadConfig; import android.system.virtualizationservice.VirtualMachineRawConfig; import android.text.TextUtils; import android.util.Log; import com.android.system.virtualmachine.flags.Flags; 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.OutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.zip.ZipFile; /** * Represents a configuration of a virtual machine. A configuration consists of hardware * configurations like the number of CPUs and the size of RAM, and software configurations like the * payload to run on the virtual machine. * * @hide */ @SystemApi public final class VirtualMachineConfig { private static final String TAG = "VirtualMachineConfig"; private static String[] EMPTY_STRING_ARRAY = {}; private static final String U_BOOT_PREBUILT_PATH = "/apex/com.android.virt/etc/u-boot.bin"; // These define the schema of the config file persisted on disk. // Please bump up the version number when adding a new key. private static final int VERSION = 9; private static final String KEY_VERSION = "version"; private static final String KEY_PACKAGENAME = "packageName"; private static final String KEY_APKPATH = "apkPath"; private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath"; private static final String KEY_CUSTOMIMAGECONFIG = "customImageConfig"; private static final String KEY_PAYLOADBINARYNAME = "payloadBinaryPath"; private static final String KEY_DEBUGLEVEL = "debugLevel"; private static final String KEY_PROTECTED_VM = "protectedVm"; private static final String KEY_MEMORY_BYTES = "memoryBytes"; private static final String KEY_CPU_TOPOLOGY = "cpuTopology"; private static final String KEY_CONSOLE_INPUT_DEVICE = "consoleInputDevice"; private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes"; private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured"; private static final String KEY_VM_CONSOLE_INPUT_SUPPORTED = "vmConsoleInputSupported"; private static final String KEY_CONNECT_VM_CONSOLE = "connectVmConsole"; private static final String KEY_VENDOR_DISK_IMAGE_PATH = "vendorDiskImagePath"; private static final String KEY_OS = "os"; private static final String KEY_EXTRA_APKS = "extraApks"; private static final String KEY_NETWORK_SUPPORTED = "networkSupported"; private static final String KEY_SHOULD_BOOST_UCLAMP = "shouldBoostUclamp"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "DEBUG_LEVEL_", value = { DEBUG_LEVEL_NONE, DEBUG_LEVEL_FULL }) public @interface DebugLevel {} /** * Not debuggable at all. No log is exported from the VM. Debugger can't be attached to the app * process running in the VM. This is the default level. * * @hide */ @SystemApi public static final int DEBUG_LEVEL_NONE = 0; /** * Fully debuggable. All logs (both logcat and kernel message) are exported. All processes * running in the VM can be attached to the debugger. Rooting is possible. * * @hide */ @SystemApi public static final int DEBUG_LEVEL_FULL = 1; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = "CPU_TOPOLOGY_", value = { CPU_TOPOLOGY_ONE_CPU, CPU_TOPOLOGY_MATCH_HOST, }) public @interface CpuTopology {} /** * Run VM with 1 vCPU. This is the default option, usually the fastest to boot and consuming the * least amount of resources. Typically the best option for small or ephemeral workloads. * * @hide */ @SystemApi public static final int CPU_TOPOLOGY_ONE_CPU = 0; /** * Run VM with vCPU topology matching the physical CPU topology of the host. Usually takes * longer to boot and consumes more resources compared to a single vCPU. Typically a good option * for long-running workloads that benefit from parallel execution. * * @hide */ @SystemApi public static final int CPU_TOPOLOGY_MATCH_HOST = 1; /** Name of a package whose primary APK contains the VM payload. */ @Nullable private final String mPackageName; /** Absolute path to the APK file containing the VM payload. */ @Nullable private final String mApkPath; private final List mExtraApks; @DebugLevel private final int mDebugLevel; /** * Whether to run the VM in protected mode, so the host can't access its memory. */ private final boolean mProtectedVm; /** * The amount of RAM to give the VM, in bytes. If this is 0 or negative the default will be * used. */ private final long mMemoryBytes; /** CPU topology configuration of the VM. */ @CpuTopology private final int mCpuTopology; /** The serial device for VM console input. */ @Nullable private final String mConsoleInputDevice; /** * Path within the APK to the payload config file that defines software aspects of the VM. */ @Nullable private final String mPayloadConfigPath; /** Name of the payload binary file within the APK that will be executed within the VM. */ @Nullable private final String mPayloadBinaryName; /** The custom image config file to launch the custom VM. */ @Nullable private final VirtualMachineCustomImageConfig mCustomImageConfig; /** The size of storage in bytes. 0 indicates that encryptedStorage is not required */ private final long mEncryptedStorageBytes; /** Whether the app can read console and log output. */ private final boolean mVmOutputCaptured; /** Whether the app can write console input to the VM */ private final boolean mVmConsoleInputSupported; /** Whether to connect the VM console to a host console. */ private final boolean mConnectVmConsole; @Nullable private final File mVendorDiskImage; /** OS name of the VM using payload binaries. */ @NonNull @OsName private final String mOs; /** Whether to run the VM with supporting network feature or not. */ private final boolean mNetworkSupported; private final boolean mShouldBoostUclamp; @Retention(RetentionPolicy.SOURCE) @StringDef( prefix = "MICRODROID", value = {MICRODROID}) private @interface OsName {} /** * OS name of microdroid using microdroid kernel. * * @see Builder#setOs * @hide */ @TestApi @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) @OsName public static final String MICRODROID = "microdroid"; private VirtualMachineConfig( @Nullable String packageName, @Nullable String apkPath, List extraApks, @Nullable String payloadConfigPath, @Nullable String payloadBinaryName, @Nullable VirtualMachineCustomImageConfig customImageConfig, @DebugLevel int debugLevel, boolean protectedVm, long memoryBytes, @CpuTopology int cpuTopology, @Nullable String consoleInputDevice, long encryptedStorageBytes, boolean vmOutputCaptured, boolean vmConsoleInputSupported, boolean connectVmConsole, @Nullable File vendorDiskImage, @NonNull @OsName String os, boolean networkSupported, boolean shouldBoostUclamp) { // This is only called from Builder.build(); the builder handles parameter validation. mPackageName = packageName; mApkPath = apkPath; mExtraApks = extraApks.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList( Arrays.asList(extraApks.toArray(new String[0]))); mPayloadConfigPath = payloadConfigPath; mPayloadBinaryName = payloadBinaryName; mCustomImageConfig = customImageConfig; mDebugLevel = debugLevel; mProtectedVm = protectedVm; mMemoryBytes = memoryBytes; mCpuTopology = cpuTopology; mConsoleInputDevice = consoleInputDevice; mEncryptedStorageBytes = encryptedStorageBytes; mVmOutputCaptured = vmOutputCaptured; mVmConsoleInputSupported = vmConsoleInputSupported; mConnectVmConsole = connectVmConsole; mVendorDiskImage = vendorDiskImage; mOs = os; mNetworkSupported = networkSupported; mShouldBoostUclamp = shouldBoostUclamp; } /** Loads a config from a file. */ @NonNull static VirtualMachineConfig from(@NonNull File file) throws VirtualMachineException { try (FileInputStream input = new FileInputStream(file)) { return fromInputStream(input); } catch (IOException e) { throw new VirtualMachineException("Failed to read VM config from file", e); } } /** Loads a config from a {@link ParcelFileDescriptor}. */ @NonNull static VirtualMachineConfig from(@NonNull ParcelFileDescriptor fd) throws VirtualMachineException { try (AutoCloseInputStream input = new AutoCloseInputStream(fd)) { return fromInputStream(input); } catch (IOException e) { throw new VirtualMachineException("failed to read VM config from file descriptor", e); } } /** Loads a config from a stream, for example a file. */ @NonNull private static VirtualMachineConfig fromInputStream(@NonNull InputStream input) throws IOException, VirtualMachineException { PersistableBundle b = PersistableBundle.readFromStream(input); try { return fromPersistableBundle(b); } catch (NullPointerException | IllegalArgumentException | IllegalStateException e) { throw new VirtualMachineException("Persisted VM config is invalid", e); } } @NonNull private static VirtualMachineConfig fromPersistableBundle(PersistableBundle b) { int version = b.getInt(KEY_VERSION); if (version > VERSION) { throw new IllegalArgumentException( "Version " + version + " too high; current is " + VERSION); } String packageName = b.getString(KEY_PACKAGENAME); Builder builder = new Builder(packageName); String apkPath = b.getString(KEY_APKPATH); if (apkPath != null) { builder.setApkPath(apkPath); } String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH); String payloadBinaryName = b.getString(KEY_PAYLOADBINARYNAME); PersistableBundle customImageConfigBundle = b.getPersistableBundle(KEY_CUSTOMIMAGECONFIG); if (customImageConfigBundle != null) { builder.setCustomImageConfig( VirtualMachineCustomImageConfig.from(customImageConfigBundle)); } else if (payloadConfigPath != null) { builder.setPayloadConfigPath(payloadConfigPath); } else { builder.setPayloadBinaryName(payloadBinaryName); } @DebugLevel int debugLevel = b.getInt(KEY_DEBUGLEVEL); if (debugLevel != DEBUG_LEVEL_NONE && debugLevel != DEBUG_LEVEL_FULL) { throw new IllegalArgumentException("Invalid debugLevel: " + debugLevel); } builder.setDebugLevel(debugLevel); builder.setProtectedVm(b.getBoolean(KEY_PROTECTED_VM)); long memoryBytes = b.getLong(KEY_MEMORY_BYTES); if (memoryBytes != 0) { builder.setMemoryBytes(memoryBytes); } builder.setCpuTopology(b.getInt(KEY_CPU_TOPOLOGY)); String consoleInputDevice = b.getString(KEY_CONSOLE_INPUT_DEVICE); if (consoleInputDevice != null) { builder.setConsoleInputDevice(consoleInputDevice); } long encryptedStorageBytes = b.getLong(KEY_ENCRYPTED_STORAGE_BYTES); if (encryptedStorageBytes != 0) { builder.setEncryptedStorageBytes(encryptedStorageBytes); } builder.setVmOutputCaptured(b.getBoolean(KEY_VM_OUTPUT_CAPTURED)); builder.setVmConsoleInputSupported(b.getBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED)); builder.setConnectVmConsole(b.getBoolean(KEY_CONNECT_VM_CONSOLE)); String vendorDiskImagePath = b.getString(KEY_VENDOR_DISK_IMAGE_PATH); if (vendorDiskImagePath != null) { builder.setVendorDiskImage(new File(vendorDiskImagePath)); } builder.setOs(b.getString(KEY_OS)); String[] extraApks = b.getStringArray(KEY_EXTRA_APKS); if (extraApks != null) { for (String extraApk : extraApks) { builder.addExtraApk(extraApk); } } builder.setNetworkSupported(b.getBoolean(KEY_NETWORK_SUPPORTED)); builder.setShouldBoostUclamp(b.getBoolean(KEY_SHOULD_BOOST_UCLAMP)); return builder.build(); } /** Persists this config to a file. */ void serialize(@NonNull File file) throws VirtualMachineException { try (FileOutputStream output = new FileOutputStream(file)) { serializeOutputStream(output); } catch (IOException e) { throw new VirtualMachineException("failed to write VM config", e); } } /** Persists this config to a stream, for example a file. */ private void serializeOutputStream(@NonNull OutputStream output) throws IOException { PersistableBundle b = new PersistableBundle(); b.putInt(KEY_VERSION, VERSION); if (mPackageName != null) { b.putString(KEY_PACKAGENAME, mPackageName); } if (mApkPath != null) { b.putString(KEY_APKPATH, mApkPath); } b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath); b.putString(KEY_PAYLOADBINARYNAME, mPayloadBinaryName); if (mCustomImageConfig != null) { b.putPersistableBundle(KEY_CUSTOMIMAGECONFIG, mCustomImageConfig.toPersistableBundle()); } b.putInt(KEY_DEBUGLEVEL, mDebugLevel); b.putBoolean(KEY_PROTECTED_VM, mProtectedVm); b.putInt(KEY_CPU_TOPOLOGY, mCpuTopology); if (mConsoleInputDevice != null) { b.putString(KEY_CONSOLE_INPUT_DEVICE, mConsoleInputDevice); } if (mMemoryBytes > 0) { b.putLong(KEY_MEMORY_BYTES, mMemoryBytes); } if (mEncryptedStorageBytes > 0) { b.putLong(KEY_ENCRYPTED_STORAGE_BYTES, mEncryptedStorageBytes); } b.putBoolean(KEY_VM_OUTPUT_CAPTURED, mVmOutputCaptured); b.putBoolean(KEY_VM_CONSOLE_INPUT_SUPPORTED, mVmConsoleInputSupported); b.putBoolean(KEY_CONNECT_VM_CONSOLE, mConnectVmConsole); if (mVendorDiskImage != null) { b.putString(KEY_VENDOR_DISK_IMAGE_PATH, mVendorDiskImage.getAbsolutePath()); } b.putString(KEY_OS, mOs); if (!mExtraApks.isEmpty()) { String[] extraApks = mExtraApks.toArray(new String[0]); b.putStringArray(KEY_EXTRA_APKS, extraApks); } b.putBoolean(KEY_NETWORK_SUPPORTED, mNetworkSupported); b.putBoolean(KEY_SHOULD_BOOST_UCLAMP, mShouldBoostUclamp); b.writeToStream(output); } /** * Returns the absolute path of the APK which should contain the binary payload that will * execute within the VM. Returns null if no specific path has been set. * * @hide */ @SystemApi @Nullable public String getApkPath() { return mApkPath; } /** * Returns the package names of any extra APKs that have been requested for the VM. They are * returned in the order in which they were added via {@link Builder#addExtraApk}. * * @hide */ @TestApi @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) @NonNull public List getExtraApks() { return mExtraApks; } /** * Returns the path within the APK to the payload config file that defines software aspects of * the VM. * * @hide */ @TestApi @Nullable public String getPayloadConfigPath() { return mPayloadConfigPath; } /** * Returns the custom image config to launch the custom VM. * * @hide */ @Nullable public VirtualMachineCustomImageConfig getCustomImageConfig() { return mCustomImageConfig; } /** * Returns the name of the payload binary file, in the {@code lib/} directory of the APK, * that will be executed within the VM. * * @hide */ @SystemApi @Nullable public String getPayloadBinaryName() { return mPayloadBinaryName; } /** * Returns the debug level for the VM. * * @hide */ @SystemApi @DebugLevel public int getDebugLevel() { return mDebugLevel; } /** * Returns whether the VM's memory will be protected from the host. * * @hide */ @SystemApi public boolean isProtectedVm() { return mProtectedVm; } /** * Returns the amount of RAM that will be made available to the VM, or 0 if the default size * will be used. * * @hide */ @SystemApi @IntRange(from = 0) public long getMemoryBytes() { return mMemoryBytes; } /** * Returns the CPU topology configuration of the VM. * * @hide */ @SystemApi @CpuTopology public int getCpuTopology() { return mCpuTopology; } /** * Returns whether encrypted storage is enabled or not. * * @hide */ @SystemApi public boolean isEncryptedStorageEnabled() { return mEncryptedStorageBytes > 0; } /** * Returns the size of encrypted storage (in bytes) available in the VM, or 0 if encrypted * storage is not enabled * * @hide */ @SystemApi @IntRange(from = 0) public long getEncryptedStorageBytes() { return mEncryptedStorageBytes; } /** * Returns whether the app can read the VM console or log output. If not, the VM output is * automatically forwarded to the host logcat. * * @see Builder#setVmOutputCaptured * @hide */ @SystemApi public boolean isVmOutputCaptured() { return mVmOutputCaptured; } /** * Returns whether the app can write to the VM console. * * @see Builder#setVmConsoleInputSupported * @hide */ @TestApi public boolean isVmConsoleInputSupported() { return mVmConsoleInputSupported; } /** * Returns whether to connect the VM console to a host console. * * @see Builder#setConnectVmConsole * @hide */ public boolean isConnectVmConsole() { return mConnectVmConsole; } /** * Returns the OS of the VM. * * @see Builder#setOs * @hide */ @TestApi @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) @NonNull @OsName public String getOs() { return mOs; } /** * Returns whether the network feature is supported to the VM or not. * * @hide */ @TestApi public boolean isNetworkSupported() { return mNetworkSupported; } /** * Tests if this config is compatible with other config. Being compatible means that the configs * can be interchangeably used for the same virtual machine; they do not change the VM identity * or secrets. Such changes include varying the number of CPUs or the size of the RAM. Changes * that would alter the identity of the VM (e.g. using a different payload or changing the debug * mode) are considered incompatible. * * @see VirtualMachine#setConfig * @hide */ @SystemApi public boolean isCompatibleWith(@NonNull VirtualMachineConfig other) { if (this == other) { return true; } return this.mDebugLevel == other.mDebugLevel && this.mProtectedVm == other.mProtectedVm && this.mEncryptedStorageBytes == other.mEncryptedStorageBytes && this.mVmOutputCaptured == other.mVmOutputCaptured && this.mVmConsoleInputSupported == other.mVmConsoleInputSupported && this.mConnectVmConsole == other.mConnectVmConsole && this.mConsoleInputDevice == other.mConsoleInputDevice && (this.mVendorDiskImage == null) == (other.mVendorDiskImage == null) && Objects.equals(this.mPayloadConfigPath, other.mPayloadConfigPath) && Objects.equals(this.mPayloadBinaryName, other.mPayloadBinaryName) && Objects.equals(this.mPackageName, other.mPackageName) && Objects.equals(this.mOs, other.mOs) && Objects.equals(this.mExtraApks, other.mExtraApks); } private ParcelFileDescriptor openOrNull(File file, int mode) { try { return ParcelFileDescriptor.open(file, mode); } catch (FileNotFoundException e) { Log.d(TAG, "cannot open", e); return null; } } VirtualMachineRawConfig toVsRawConfig() throws IllegalStateException, IOException { VirtualMachineRawConfig config = new VirtualMachineRawConfig(); VirtualMachineCustomImageConfig customImageConfig = getCustomImageConfig(); requireNonNull(customImageConfig); config.name = Optional.ofNullable(customImageConfig.getName()).orElse(""); config.instanceId = new byte[64]; config.kernel = Optional.ofNullable(customImageConfig.getKernelPath()) .map( (path) -> { try { return ParcelFileDescriptor.open( new File(path), MODE_READ_ONLY); } catch (FileNotFoundException e) { throw new RuntimeException(e); } }) .orElse(null); config.initrd = Optional.ofNullable(customImageConfig.getInitrdPath()) .map((path) -> openOrNull(new File(path), MODE_READ_ONLY)) .orElse(null); config.bootloader = Optional.ofNullable(customImageConfig.getBootloaderPath()) .map((path) -> openOrNull(new File(path), MODE_READ_ONLY)) .orElse(null); if (config.kernel == null && config.bootloader == null) { config.bootloader = openOrNull(new File(U_BOOT_PREBUILT_PATH), MODE_READ_ONLY); } config.params = Optional.ofNullable(customImageConfig.getParams()) .map((params) -> TextUtils.join(" ", params)) .orElse(""); config.disks = new DiskImage [Optional.ofNullable(customImageConfig.getDisks()) .map(arr -> arr.length) .orElse(0)]; for (int i = 0; i < config.disks.length; i++) { config.disks[i] = new DiskImage(); config.disks[i].writable = customImageConfig.getDisks()[i].isWritable(); config.disks[i].image = ParcelFileDescriptor.open( new File(customImageConfig.getDisks()[i].getImagePath()), config.disks[i].writable ? MODE_READ_WRITE : MODE_READ_ONLY); config.disks[i].partitions = new Partition[0]; } config.displayConfig = Optional.ofNullable(customImageConfig.getDisplayConfig()) .map(dc -> dc.toParcelable()) .orElse(null); config.gpuConfig = Optional.ofNullable(customImageConfig.getGpuConfig()) .map(dc -> dc.toParcelable()) .orElse(null); config.protectedVm = this.mProtectedVm; config.memoryMib = bytesToMebiBytes(mMemoryBytes); config.cpuTopology = (byte) this.mCpuTopology; config.consoleInputDevice = mConsoleInputDevice; config.devices = EMPTY_STRING_ARRAY; config.networkSupported = this.mNetworkSupported; config.platformVersion = "~1.0"; return config; } /** * Converts this config object into the parcelable type used when creating a VM via the * virtualization service. Notice that the files are not passed as paths, but as file * descriptors because the service doesn't accept paths as it might not have permission to open * app-owned files and that could be abused to run a VM with software that the calling * application doesn't own. */ VirtualMachineAppConfig toVsConfig(@NonNull PackageManager packageManager) throws VirtualMachineException { VirtualMachineAppConfig vsConfig = new VirtualMachineAppConfig(); String apkPath = (mApkPath != null) ? mApkPath : findPayloadApk(packageManager); try { vsConfig.apk = ParcelFileDescriptor.open(new File(apkPath), MODE_READ_ONLY); } catch (FileNotFoundException e) { throw new VirtualMachineException("Failed to open APK", e); } if (mPayloadBinaryName != null) { VirtualMachinePayloadConfig payloadConfig = new VirtualMachinePayloadConfig(); payloadConfig.payloadBinaryName = mPayloadBinaryName; payloadConfig.extraApks = Collections.emptyList(); vsConfig.payload = VirtualMachineAppConfig.Payload.payloadConfig(payloadConfig); } else { vsConfig.payload = VirtualMachineAppConfig.Payload.configPath(mPayloadConfigPath); } vsConfig.osName = mOs; switch (mDebugLevel) { case DEBUG_LEVEL_FULL: vsConfig.debugLevel = VirtualMachineAppConfig.DebugLevel.FULL; break; default: vsConfig.debugLevel = VirtualMachineAppConfig.DebugLevel.NONE; break; } vsConfig.protectedVm = mProtectedVm; vsConfig.memoryMib = bytesToMebiBytes(mMemoryBytes); switch (mCpuTopology) { case CPU_TOPOLOGY_MATCH_HOST: vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.MATCH_HOST; break; default: vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.ONE_CPU; break; } if (mVendorDiskImage != null || mNetworkSupported) { VirtualMachineAppConfig.CustomConfig customConfig = new VirtualMachineAppConfig.CustomConfig(); customConfig.devices = EMPTY_STRING_ARRAY; if (mVendorDiskImage != null) { try { customConfig.vendorImage = ParcelFileDescriptor.open(mVendorDiskImage, MODE_READ_ONLY); } catch (FileNotFoundException e) { throw new VirtualMachineException( "Failed to open vendor disk image " + mVendorDiskImage.getAbsolutePath(), e); } } customConfig.networkSupported = mNetworkSupported; vsConfig.customConfig = customConfig; } vsConfig.boostUclamp = mShouldBoostUclamp; return vsConfig; } private String findPayloadApk(PackageManager packageManager) throws VirtualMachineException { ApplicationInfo appInfo; try { appInfo = packageManager.getApplicationInfo( mPackageName, PackageManager.ApplicationInfoFlags.of(0)); } catch (PackageManager.NameNotFoundException e) { throw new VirtualMachineException("Package not found", e); } String[] splitApkPaths = appInfo.splitSourceDirs; String[] abis = Build.SUPPORTED_64_BIT_ABIS; // If there are split APKs, and we know the payload binary name, see if we can find a // split APK containing the binary. if (mPayloadBinaryName != null && splitApkPaths != null && abis.length != 0) { String[] libraryNames = new String[abis.length]; for (int i = 0; i < abis.length; i++) { libraryNames[i] = "lib/" + abis[i] + "/" + mPayloadBinaryName; } for (String path : splitApkPaths) { try (ZipFile zip = new ZipFile(path)) { for (String name : libraryNames) { if (zip.getEntry(name) != null) { Log.i(TAG, "Found payload in " + path); return path; } } } catch (IOException e) { Log.w(TAG, "Failed to scan split APK: " + path, e); } } } // This really is the path to the APK, not a directory. return appInfo.sourceDir; } private int bytesToMebiBytes(long mMemoryBytes) { long oneMebi = 1024 * 1024; // We can't express requests for more than 2 exabytes, but then they're not going to succeed // anyway. if (mMemoryBytes > (Integer.MAX_VALUE - 1) * oneMebi) { return Integer.MAX_VALUE; } return (int) ((mMemoryBytes + oneMebi - 1) / oneMebi); } /** * A builder used to create a {@link VirtualMachineConfig}. * * @hide */ @SystemApi public static final class Builder { @OsName private final String DEFAULT_OS = MICRODROID; @Nullable private final String mPackageName; @Nullable private String mApkPath; private final List mExtraApks = new ArrayList<>(); @Nullable private String mPayloadConfigPath; @Nullable private VirtualMachineCustomImageConfig mCustomImageConfig; @Nullable private String mPayloadBinaryName; @DebugLevel private int mDebugLevel = DEBUG_LEVEL_NONE; private boolean mProtectedVm; private boolean mProtectedVmSet; private long mMemoryBytes; @CpuTopology private int mCpuTopology = CPU_TOPOLOGY_ONE_CPU; @Nullable private String mConsoleInputDevice; private long mEncryptedStorageBytes; private boolean mVmOutputCaptured = false; private boolean mVmConsoleInputSupported = false; private boolean mConnectVmConsole = false; @Nullable private File mVendorDiskImage; @NonNull @OsName private String mOs = DEFAULT_OS; private boolean mNetworkSupported; private boolean mShouldBoostUclamp = false; /** * Creates a builder for the given context. * * @hide */ @SystemApi public Builder(@NonNull Context context) { mPackageName = requireNonNull(context, "context must not be null").getPackageName(); } /** * Creates a builder for a specific package. If packageName is null, {@link #setApkPath} * must be called to specify the APK containing the payload. */ private Builder(@Nullable String packageName) { mPackageName = packageName; } /** * Builds an immutable {@link VirtualMachineConfig} * * @hide */ @SystemApi @NonNull public VirtualMachineConfig build() { String apkPath = null; String packageName = null; if (mApkPath != null) { apkPath = mApkPath; } else if (mPackageName != null) { packageName = mPackageName; } else { // This should never happen, unless we're deserializing a bad config throw new IllegalStateException("apkPath or packageName must be specified"); } if (mCustomImageConfig != null) { if (mPayloadBinaryName != null || mPayloadConfigPath != null) { throw new IllegalStateException( "setCustomImageConfig and (setPayloadBinaryName or" + " setPayloadConfigPath) may not both be called"); } } else if (mPayloadBinaryName == null) { if (mPayloadConfigPath == null) { throw new IllegalStateException("setPayloadBinaryName must be called"); } if (!mExtraApks.isEmpty()) { throw new IllegalStateException( "setPayloadConfigPath and addExtraApk may not both be called"); } } else { if (mPayloadConfigPath != null) { throw new IllegalStateException( "setPayloadBinaryName and setPayloadConfigPath may not both be called"); } } if (!mProtectedVmSet) { throw new IllegalStateException("setProtectedVm must be called explicitly"); } if (mVmOutputCaptured && mDebugLevel != DEBUG_LEVEL_FULL) { throw new IllegalStateException("debug level must be FULL to capture output"); } if (mVmConsoleInputSupported && mDebugLevel != DEBUG_LEVEL_FULL) { throw new IllegalStateException("debug level must be FULL to use console input"); } if (mConnectVmConsole && mDebugLevel != DEBUG_LEVEL_FULL) { throw new IllegalStateException( "debug level must be FULL to connect to the console"); } if (mNetworkSupported && mProtectedVm) { throw new IllegalStateException("network is not supported on pVM"); } return new VirtualMachineConfig( packageName, apkPath, mExtraApks, mPayloadConfigPath, mPayloadBinaryName, mCustomImageConfig, mDebugLevel, mProtectedVm, mMemoryBytes, mCpuTopology, mConsoleInputDevice, mEncryptedStorageBytes, mVmOutputCaptured, mVmConsoleInputSupported, mConnectVmConsole, mVendorDiskImage, mOs, mNetworkSupported, mShouldBoostUclamp); } /** * Sets the absolute path of the APK containing the binary payload that will execute within * the VM. If not set explicitly, defaults to the split APK containing the payload, if there * is one, and otherwise the primary APK of the context. * * @hide */ @SystemApi @NonNull public Builder setApkPath(@NonNull String apkPath) { requireNonNull(apkPath, "apkPath must not be null"); if (!apkPath.startsWith("/")) { throw new IllegalArgumentException("APK path must be an absolute path"); } mApkPath = apkPath; return this; } /** * Specify the package name of an extra APK to be included in the VM. Each extra APK is * mounted, in unzipped form, inside the VM, allowing access to the code and/or data within * it. The VM entry point must be in the main APK. * * @hide */ @TestApi @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) @NonNull public Builder addExtraApk(@NonNull String packageName) { mExtraApks.add(requireNonNull(packageName, "extra APK package name must not be null")); return this; } /** * Sets the path within the APK to the payload config file that defines software aspects of * the VM. The file is a JSON file; see * packages/modules/Virtualization/microdroid/payload/config/src/lib.rs for the format. * * @hide */ @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) @TestApi @NonNull public Builder setPayloadConfigPath(@NonNull String payloadConfigPath) { mPayloadConfigPath = requireNonNull(payloadConfigPath, "payloadConfigPath must not be null"); return this; } /** * Sets the custom config file to launch the custom VM. * * @hide */ @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) @NonNull public Builder setCustomImageConfig( @NonNull VirtualMachineCustomImageConfig customImageConfig) { this.mCustomImageConfig = customImageConfig; return this; } /** * Sets the name of the payload binary file that will be executed within the VM, e.g. * "payload.so". The file must reside in the {@code lib/} directory of the APK. * *

Note that VMs only support 64-bit code, even if the owning app is running as a 32-bit * process. * * @hide */ @SystemApi @NonNull public Builder setPayloadBinaryName(@NonNull String payloadBinaryName) { requireNonNull(payloadBinaryName, "payloadBinaryName must not be null"); if (payloadBinaryName.contains(File.separator)) { throw new IllegalArgumentException( "Invalid binary file name: " + payloadBinaryName); } mPayloadBinaryName = payloadBinaryName; return this; } /** * Sets the debug level. Defaults to {@link #DEBUG_LEVEL_NONE}. * *

If {@link #DEBUG_LEVEL_FULL} is set then logs from inside the VM are exported to the * host and adb connections from the host are possible. This is convenient for debugging but * may compromise the integrity of the VM - including bypassing the protections offered by a * {@linkplain #setProtectedVm protected VM}. * *

Note that it isn't possible to {@linkplain #isCompatibleWith change} the debug level * of a VM instance; debug and non-debug VMs always have different secrets. * * @hide */ @SystemApi @NonNull public Builder setDebugLevel(@DebugLevel int debugLevel) { if (debugLevel != DEBUG_LEVEL_NONE && debugLevel != DEBUG_LEVEL_FULL) { throw new IllegalArgumentException("Invalid debugLevel: " + debugLevel); } mDebugLevel = debugLevel; return this; } /** * Sets whether to protect the VM memory from the host. No default is provided, this must be * set explicitly. * *

Note that if debugging is {@linkplain #setDebugLevel enabled} for a protected VM, the * VM is not truly protected - direct memory access by the host is prevented, but e.g. the * debugger can be used to access the VM's internals. * *

It isn't possible to {@linkplain #isCompatibleWith change} the protected status of a * VM instance; protected and non-protected VMs always have different secrets. * * @see VirtualMachineManager#getCapabilities * @hide */ @SystemApi @NonNull public Builder setProtectedVm(boolean protectedVm) { if (protectedVm) { if (!HypervisorProperties.hypervisor_protected_vm_supported().orElse(false)) { throw new UnsupportedOperationException( "Protected VMs are not supported on this device."); } } else { if (!HypervisorProperties.hypervisor_vm_supported().orElse(false)) { throw new UnsupportedOperationException( "Non-protected VMs are not supported on this device."); } } mProtectedVm = protectedVm; mProtectedVmSet = true; return this; } /** * Sets the amount of RAM to give the VM, in bytes. If not explicitly set then a default * size will be used. * * @hide */ @SystemApi @NonNull public Builder setMemoryBytes(@IntRange(from = 1) long memoryBytes) { if (memoryBytes <= 0) { throw new IllegalArgumentException("Memory size must be positive"); } mMemoryBytes = memoryBytes; return this; } /** * Sets the CPU topology configuration of the VM. Defaults to {@link #CPU_TOPOLOGY_ONE_CPU}. * *

This determines how many virtual CPUs will be created, and their performance and * scheduling characteristics, such as affinity masks. Topology also has an effect on memory * usage as each vCPU requires additional memory to keep its state. * * @hide */ @SystemApi @NonNull public Builder setCpuTopology(@CpuTopology int cpuTopology) { if (cpuTopology != CPU_TOPOLOGY_ONE_CPU && cpuTopology != CPU_TOPOLOGY_MATCH_HOST) { throw new IllegalArgumentException("Invalid cpuTopology: " + cpuTopology); } mCpuTopology = cpuTopology; return this; } /** * Sets the serial device for VM console input. * * @see android.system.virtualizationservice.ConsoleInputDevice * @hide */ public Builder setConsoleInputDevice(@Nullable String consoleInputDevice) { mConsoleInputDevice = consoleInputDevice; return this; } /** * Sets the size (in bytes) of encrypted storage available to the VM. If not set, no * encrypted storage is provided. * *

The storage is encrypted with a key deterministically derived from the VM identity * *

The encrypted storage is persistent across VM reboots as well as device reboots. The * backing file (containing encrypted data) is stored in the app's private data directory. * *

Note - There is no integrity guarantee or rollback protection on the storage in case * the encrypted data is modified. * *

Deleting the VM will delete the encrypted data - there is no way to recover that data. * * @hide */ @SystemApi @NonNull public Builder setEncryptedStorageBytes(@IntRange(from = 1) long encryptedStorageBytes) { if (encryptedStorageBytes <= 0) { throw new IllegalArgumentException("Encrypted Storage size must be positive"); } mEncryptedStorageBytes = encryptedStorageBytes; return this; } /** * Sets whether to allow the app to read the VM outputs (console / log). Default is {@code * false}. * *

By default, console and log outputs of a {@linkplain #setDebugLevel debuggable} VM are * automatically forwarded to the host logcat. Setting this as {@code true} will allow the * app to directly read {@linkplain VirtualMachine#getConsoleOutput console output} and * {@linkplain VirtualMachine#getLogOutput log output}, instead of forwarding them to the * host logcat. * *

If you turn on output capture, you must consume data from {@link * VirtualMachine#getConsoleOutput} and {@link VirtualMachine#getLogOutput} - because * otherwise the code in the VM may get blocked when the pipe buffer fills up. * *

The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be * set as true. * * @hide */ @SystemApi @NonNull public Builder setVmOutputCaptured(boolean captured) { mVmOutputCaptured = captured; return this; } /** * Sets whether to allow the app to write to the VM console. Default is {@code false}. * *

Setting this as {@code true} will allow the app to directly write into {@linkplain * VirtualMachine#getConsoleInput console input}. * *

The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be * set as true. * * @hide */ @TestApi @NonNull public Builder setVmConsoleInputSupported(boolean supported) { mVmConsoleInputSupported = supported; return this; } /** * Sets whether to connect the VM console to a host console. Default is {@code false}. * *

Setting this as {@code true} will allow the shell to directly communicate with the VM * console through the connected host console. * *

The {@linkplain #setDebugLevel debug level} must be {@link #DEBUG_LEVEL_FULL} to be * set as true. * * @hide */ @NonNull public Builder setConnectVmConsole(boolean supported) { mConnectVmConsole = supported; return this; } /** * Sets the path to the disk image with vendor-specific modules. * * @hide */ @TestApi @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) @NonNull public Builder setVendorDiskImage(@NonNull File vendorDiskImage) { mVendorDiskImage = requireNonNull(vendorDiskImage, "vendor disk image must not be null"); return this; } /** * Sets an OS for the VM. Defaults to {@code "microdroid"}. * *

See {@link VirtualMachineManager#getSupportedOSList} for available OS names. * * @hide */ @TestApi @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) @RequiresPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) @NonNull public Builder setOs(@NonNull @OsName String os) { mOs = requireNonNull(os, "os must not be null"); return this; } /** * Sets whether to support network feature to VM. Default is {@code false}. * * @hide */ @TestApi @RequiresPermission( allOf = { VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION, Manifest.permission.INTERNET }) @NonNull public Builder setNetworkSupported(boolean networkSupported) { mNetworkSupported = networkSupported; return this; } /** @hide */ public Builder setShouldBoostUclamp(boolean shouldBoostUclamp) { mShouldBoostUclamp = shouldBoostUclamp; return this; } } }