/* * 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.MODE_READ_ONLY; import android.annotation.NonNull; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.Signature; // This actually is certificate! import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.sysprop.HypervisorProperties; import android.system.virtualizationservice.VirtualMachineAppConfig; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; /** * 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 * OS and application to run on the virtual machine. * * @hide */ public final class VirtualMachineConfig { // These defines the schema of the config file persisted on disk. private static final int VERSION = 1; private static final String KEY_VERSION = "version"; private static final String KEY_CERTS = "certs"; private static final String KEY_APKPATH = "apkPath"; private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath"; private static final String KEY_DEBUGLEVEL = "debugLevel"; private static final String KEY_PROTECTED_VM = "protectedVm"; private static final String KEY_MEMORY_MIB = "memoryMib"; private static final String KEY_NUM_CPUS = "numCpus"; private static final String KEY_CPU_AFFINITY = "cpuAffinity"; // Paths to the APK file of this application. private final @NonNull String mApkPath; private final @NonNull Signature[] mCerts; /** A debug level defines the set of debug features that the VM can be configured to. */ public enum 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. */ NONE, /** * Only the app is debuggable. Log from the app is exported from the VM. Debugger can be * attached to the app process. Rest of the VM is not debuggable. */ APP_ONLY, /** * 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. */ FULL, } private final DebugLevel 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 MiB. If this is 0 or negative the default will be used. */ private final int mMemoryMib; /** * Number of vCPUs in the VM. Defaults to 1 when not specified. */ private final int mNumCpus; /** * Comma-separated list of CPUs or CPU ranges to run vCPUs on (e.g. 0,1-3,5), or * colon-separated list of assignments of vCPU to host CPU assignments (e.g. 0=0:1=1:2=2). * Default is no mask which means a vCPU can run on any host CPU. */ private final String mCpuAffinity; /** * Path within the APK to the payload config file that defines software aspects of this config. */ private final @NonNull String mPayloadConfigPath; // TODO(jiyong): add more items like # of cpu, size of ram, debuggability, etc. private VirtualMachineConfig( @NonNull String apkPath, @NonNull Signature[] certs, @NonNull String payloadConfigPath, DebugLevel debugLevel, boolean protectedVm, int memoryMib, int numCpus, String cpuAffinity) { mApkPath = apkPath; mCerts = certs; mPayloadConfigPath = payloadConfigPath; mDebugLevel = debugLevel; mProtectedVm = protectedVm; mMemoryMib = memoryMib; mNumCpus = numCpus; mCpuAffinity = cpuAffinity; } /** Loads a config from a stream, for example a file. */ /* package */ static @NonNull VirtualMachineConfig from(@NonNull InputStream input) throws IOException, VirtualMachineException { PersistableBundle b = PersistableBundle.readFromStream(input); final int version = b.getInt(KEY_VERSION); if (version > VERSION) { throw new VirtualMachineException("Version too high"); } final String apkPath = b.getString(KEY_APKPATH); if (apkPath == null) { throw new VirtualMachineException("No apkPath"); } final String[] certStrings = b.getStringArray(KEY_CERTS); if (certStrings == null || certStrings.length == 0) { throw new VirtualMachineException("No certs"); } List certList = new ArrayList<>(); for (String s : certStrings) { certList.add(new Signature(s)); } Signature[] certs = certList.toArray(new Signature[0]); final String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH); if (payloadConfigPath == null) { throw new VirtualMachineException("No payloadConfigPath"); } final DebugLevel debugLevel = DebugLevel.values()[b.getInt(KEY_DEBUGLEVEL)]; final boolean protectedVm = b.getBoolean(KEY_PROTECTED_VM); final int memoryMib = b.getInt(KEY_MEMORY_MIB); final int numCpus = b.getInt(KEY_NUM_CPUS); final String cpuAffinity = b.getString(KEY_CPU_AFFINITY); return new VirtualMachineConfig(apkPath, certs, payloadConfigPath, debugLevel, protectedVm, memoryMib, numCpus, cpuAffinity); } /** Persists this config to a stream, for example a file. */ /* package */ void serialize(@NonNull OutputStream output) throws IOException { PersistableBundle b = new PersistableBundle(); b.putInt(KEY_VERSION, VERSION); b.putString(KEY_APKPATH, mApkPath); List certList = new ArrayList<>(); for (Signature cert : mCerts) { certList.add(cert.toCharsString()); } String[] certs = certList.toArray(new String[0]); b.putStringArray(KEY_CERTS, certs); b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath); b.putInt(KEY_DEBUGLEVEL, mDebugLevel.ordinal()); b.putBoolean(KEY_PROTECTED_VM, mProtectedVm); b.putInt(KEY_NUM_CPUS, mNumCpus); if (mMemoryMib > 0) { b.putInt(KEY_MEMORY_MIB, mMemoryMib); } b.writeToStream(output); } /** Returns the path to the payload config within the owning application. */ public @NonNull String getPayloadConfigPath() { return mPayloadConfigPath; } /** * Tests if this config is compatible with other config. Being compatible means that the configs * can be interchangeably used for the same virtual machine. Compatible changes includes the * number of CPUs and the size of the RAM, and change of the payload as long as the payload is * signed by the same signer. All other changes (e.g. using a payload from a different signer, * change of the debug mode, etc.) are considered as incompatible. */ public boolean isCompatibleWith(@NonNull VirtualMachineConfig other) { if (!Arrays.equals(this.mCerts, other.mCerts)) { return false; } if (this.mDebugLevel != other.mDebugLevel) { // TODO(jiyong): should we treat APP_ONLY and FULL the same? return false; } if (this.mProtectedVm != other.mProtectedVm) { return false; } return true; } /** * Converts this config object into a parcel. 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. */ /* package */ VirtualMachineAppConfig toParcel() throws FileNotFoundException { VirtualMachineAppConfig parcel = new VirtualMachineAppConfig(); parcel.apk = ParcelFileDescriptor.open(new File(mApkPath), MODE_READ_ONLY); parcel.configPath = mPayloadConfigPath; switch (mDebugLevel) { case NONE: parcel.debugLevel = VirtualMachineAppConfig.DebugLevel.NONE; break; case APP_ONLY: parcel.debugLevel = VirtualMachineAppConfig.DebugLevel.APP_ONLY; break; case FULL: parcel.debugLevel = VirtualMachineAppConfig.DebugLevel.FULL; break; } parcel.protectedVm = mProtectedVm; parcel.memoryMib = mMemoryMib; parcel.numCpus = mNumCpus; parcel.cpuAffinity = mCpuAffinity; // Don't allow apps to set task profiles ... at last for now. Also, don't forget to // validate the string because these are appended to the cmdline argument. parcel.taskProfiles = new String[0]; return parcel; } /** A builder used to create a {@link VirtualMachineConfig}. */ public static class Builder { private Context mContext; private String mPayloadConfigPath; private DebugLevel mDebugLevel; private boolean mProtectedVm; private int mMemoryMib; private int mNumCpus; private String mCpuAffinity; /** Creates a builder for the given context (APK), and the payload config file in APK. */ public Builder(@NonNull Context context, @NonNull String payloadConfigPath) { mContext = context; mPayloadConfigPath = payloadConfigPath; mDebugLevel = DebugLevel.NONE; mProtectedVm = false; mNumCpus = 1; mCpuAffinity = null; } /** Sets the debug level */ public Builder debugLevel(DebugLevel debugLevel) { mDebugLevel = debugLevel; return this; } /** Sets whether to protect the VM memory from the host. Defaults to false. */ public Builder protectedVm(boolean protectedVm) { mProtectedVm = protectedVm; return this; } /** * Sets the amount of RAM to give the VM. If this is zero or negative then the default will * be used. */ public Builder memoryMib(int memoryMib) { mMemoryMib = memoryMib; return this; } /** * Sets the number of vCPUs in the VM. Defaults to 1. */ public Builder numCpus(int num) { mNumCpus = num; return this; } /** * Sets on which host CPUs the vCPUs can run. The format is a comma-separated list of CPUs * or CPU ranges to run vCPUs on. e.g. "0,1-3,5" to choose host CPUs 0, 1, 2, 3, and 5. * Or this can be a colon-separated list of assignments of vCPU to host CPU assignments. * e.g. "0=0:1=1:2=2" to map vCPU 0 to host CPU 0, and so on. */ public Builder cpuAffinity(String affinity) { mCpuAffinity = affinity; return this; } /** Builds an immutable {@link VirtualMachineConfig} */ public @NonNull VirtualMachineConfig build() { final String apkPath = mContext.getPackageCodePath(); final String packageName = mContext.getPackageName(); Signature[] certs; try { certs = mContext.getPackageManager() .getPackageInfo( packageName, PackageManager.GET_SIGNING_CERTIFICATES) .signingInfo .getSigningCertificateHistory(); } catch (PackageManager.NameNotFoundException e) { // This cannot happen as `packageName` is from this app. throw new RuntimeException(e); } final int availableCpus = Runtime.getRuntime().availableProcessors(); if (mNumCpus < 1 || mNumCpus > availableCpus) { throw new IllegalArgumentException("Number of vCPUs (" + mNumCpus + ") is out of " + "range [1, " + availableCpus + "]"); } if (mCpuAffinity != null && !Pattern.matches("[\\d]+(-[\\d]+)?(,[\\d]+(-[\\d]+)?)*", mCpuAffinity) && !Pattern.matches("[\\d]+=[\\d]+(:[\\d]+=[\\d]+)*", mCpuAffinity)) { throw new IllegalArgumentException("CPU affinity [" + mCpuAffinity + "]" + " is invalid"); } if (mProtectedVm && !HypervisorProperties.hypervisor_protected_vm_supported().orElse(false)) { throw new UnsupportedOperationException( "Protected VMs are not supported on this device."); } if (!mProtectedVm && !HypervisorProperties.hypervisor_vm_supported().orElse(false)) { throw new UnsupportedOperationException( "Unprotected VMs are not supported on this device."); } return new VirtualMachineConfig( apkPath, certs, mPayloadConfigPath, mDebugLevel, mProtectedVm, mMemoryMib, mNumCpus, mCpuAffinity); } } }