1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.system.virtualmachine; 18 19 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; 20 21 import android.annotation.NonNull; 22 import android.content.Context; 23 import android.content.pm.PackageManager; 24 import android.content.pm.Signature; // This actually is certificate! 25 import android.os.ParcelFileDescriptor; 26 import android.os.PersistableBundle; 27 import android.sysprop.HypervisorProperties; 28 import android.system.virtualizationservice.VirtualMachineAppConfig; 29 30 import java.io.File; 31 import java.io.FileNotFoundException; 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.io.OutputStream; 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.List; 38 import java.util.regex.Pattern; 39 40 /** 41 * Represents a configuration of a virtual machine. A configuration consists of hardware 42 * configurations like the number of CPUs and the size of RAM, and software configurations like the 43 * OS and application to run on the virtual machine. 44 * 45 * @hide 46 */ 47 public final class VirtualMachineConfig { 48 // These defines the schema of the config file persisted on disk. 49 private static final int VERSION = 1; 50 private static final String KEY_VERSION = "version"; 51 private static final String KEY_CERTS = "certs"; 52 private static final String KEY_APKPATH = "apkPath"; 53 private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath"; 54 private static final String KEY_DEBUGLEVEL = "debugLevel"; 55 private static final String KEY_PROTECTED_VM = "protectedVm"; 56 private static final String KEY_MEMORY_MIB = "memoryMib"; 57 private static final String KEY_NUM_CPUS = "numCpus"; 58 private static final String KEY_CPU_AFFINITY = "cpuAffinity"; 59 60 // Paths to the APK file of this application. 61 private final @NonNull String mApkPath; 62 private final @NonNull Signature[] mCerts; 63 64 /** A debug level defines the set of debug features that the VM can be configured to. */ 65 public enum DebugLevel { 66 /** 67 * Not debuggable at all. No log is exported from the VM. Debugger can't be attached to the 68 * app process running in the VM. This is the default level. 69 */ 70 NONE, 71 72 /** 73 * Only the app is debuggable. Log from the app is exported from the VM. Debugger can be 74 * attached to the app process. Rest of the VM is not debuggable. 75 */ 76 APP_ONLY, 77 78 /** 79 * Fully debuggable. All logs (both logcat and kernel message) are exported. All processes 80 * running in the VM can be attached to the debugger. Rooting is possible. 81 */ 82 FULL, 83 } 84 85 private final DebugLevel mDebugLevel; 86 87 /** 88 * Whether to run the VM in protected mode, so the host can't access its memory. 89 */ 90 private final boolean mProtectedVm; 91 92 /** 93 * The amount of RAM to give the VM, in MiB. If this is 0 or negative the default will be used. 94 */ 95 private final int mMemoryMib; 96 97 /** 98 * Number of vCPUs in the VM. Defaults to 1 when not specified. 99 */ 100 private final int mNumCpus; 101 102 /** 103 * Comma-separated list of CPUs or CPU ranges to run vCPUs on (e.g. 0,1-3,5), or 104 * colon-separated list of assignments of vCPU to host CPU assignments (e.g. 0=0:1=1:2=2). 105 * Default is no mask which means a vCPU can run on any host CPU. 106 */ 107 private final String mCpuAffinity; 108 109 /** 110 * Path within the APK to the payload config file that defines software aspects of this config. 111 */ 112 private final @NonNull String mPayloadConfigPath; 113 114 // TODO(jiyong): add more items like # of cpu, size of ram, debuggability, etc. 115 VirtualMachineConfig( @onNull String apkPath, @NonNull Signature[] certs, @NonNull String payloadConfigPath, DebugLevel debugLevel, boolean protectedVm, int memoryMib, int numCpus, String cpuAffinity)116 private VirtualMachineConfig( 117 @NonNull String apkPath, 118 @NonNull Signature[] certs, 119 @NonNull String payloadConfigPath, 120 DebugLevel debugLevel, 121 boolean protectedVm, 122 int memoryMib, 123 int numCpus, 124 String cpuAffinity) { 125 mApkPath = apkPath; 126 mCerts = certs; 127 mPayloadConfigPath = payloadConfigPath; 128 mDebugLevel = debugLevel; 129 mProtectedVm = protectedVm; 130 mMemoryMib = memoryMib; 131 mNumCpus = numCpus; 132 mCpuAffinity = cpuAffinity; 133 } 134 135 /** Loads a config from a stream, for example a file. */ from(@onNull InputStream input)136 /* package */ static @NonNull VirtualMachineConfig from(@NonNull InputStream input) 137 throws IOException, VirtualMachineException { 138 PersistableBundle b = PersistableBundle.readFromStream(input); 139 final int version = b.getInt(KEY_VERSION); 140 if (version > VERSION) { 141 throw new VirtualMachineException("Version too high"); 142 } 143 final String apkPath = b.getString(KEY_APKPATH); 144 if (apkPath == null) { 145 throw new VirtualMachineException("No apkPath"); 146 } 147 final String[] certStrings = b.getStringArray(KEY_CERTS); 148 if (certStrings == null || certStrings.length == 0) { 149 throw new VirtualMachineException("No certs"); 150 } 151 List<Signature> certList = new ArrayList<>(); 152 for (String s : certStrings) { 153 certList.add(new Signature(s)); 154 } 155 Signature[] certs = certList.toArray(new Signature[0]); 156 final String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH); 157 if (payloadConfigPath == null) { 158 throw new VirtualMachineException("No payloadConfigPath"); 159 } 160 final DebugLevel debugLevel = DebugLevel.values()[b.getInt(KEY_DEBUGLEVEL)]; 161 final boolean protectedVm = b.getBoolean(KEY_PROTECTED_VM); 162 final int memoryMib = b.getInt(KEY_MEMORY_MIB); 163 final int numCpus = b.getInt(KEY_NUM_CPUS); 164 final String cpuAffinity = b.getString(KEY_CPU_AFFINITY); 165 return new VirtualMachineConfig(apkPath, certs, payloadConfigPath, debugLevel, protectedVm, 166 memoryMib, numCpus, cpuAffinity); 167 } 168 169 /** Persists this config to a stream, for example a file. */ serialize(@onNull OutputStream output)170 /* package */ void serialize(@NonNull OutputStream output) throws IOException { 171 PersistableBundle b = new PersistableBundle(); 172 b.putInt(KEY_VERSION, VERSION); 173 b.putString(KEY_APKPATH, mApkPath); 174 List<String> certList = new ArrayList<>(); 175 for (Signature cert : mCerts) { 176 certList.add(cert.toCharsString()); 177 } 178 String[] certs = certList.toArray(new String[0]); 179 b.putStringArray(KEY_CERTS, certs); 180 b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath); 181 b.putInt(KEY_DEBUGLEVEL, mDebugLevel.ordinal()); 182 b.putBoolean(KEY_PROTECTED_VM, mProtectedVm); 183 b.putInt(KEY_NUM_CPUS, mNumCpus); 184 if (mMemoryMib > 0) { 185 b.putInt(KEY_MEMORY_MIB, mMemoryMib); 186 } 187 b.writeToStream(output); 188 } 189 190 /** Returns the path to the payload config within the owning application. */ getPayloadConfigPath()191 public @NonNull String getPayloadConfigPath() { 192 return mPayloadConfigPath; 193 } 194 195 /** 196 * Tests if this config is compatible with other config. Being compatible means that the configs 197 * can be interchangeably used for the same virtual machine. Compatible changes includes the 198 * number of CPUs and the size of the RAM, and change of the payload as long as the payload is 199 * signed by the same signer. All other changes (e.g. using a payload from a different signer, 200 * change of the debug mode, etc.) are considered as incompatible. 201 */ isCompatibleWith(@onNull VirtualMachineConfig other)202 public boolean isCompatibleWith(@NonNull VirtualMachineConfig other) { 203 if (!Arrays.equals(this.mCerts, other.mCerts)) { 204 return false; 205 } 206 if (this.mDebugLevel != other.mDebugLevel) { 207 // TODO(jiyong): should we treat APP_ONLY and FULL the same? 208 return false; 209 } 210 if (this.mProtectedVm != other.mProtectedVm) { 211 return false; 212 } 213 return true; 214 } 215 216 /** 217 * Converts this config object into a parcel. Used when creating a VM via the virtualization 218 * service. Notice that the files are not passed as paths, but as file descriptors because the 219 * service doesn't accept paths as it might not have permission to open app-owned files and that 220 * could be abused to run a VM with software that the calling application doesn't own. 221 */ toParcel()222 /* package */ VirtualMachineAppConfig toParcel() throws FileNotFoundException { 223 VirtualMachineAppConfig parcel = new VirtualMachineAppConfig(); 224 parcel.apk = ParcelFileDescriptor.open(new File(mApkPath), MODE_READ_ONLY); 225 parcel.configPath = mPayloadConfigPath; 226 switch (mDebugLevel) { 227 case NONE: 228 parcel.debugLevel = VirtualMachineAppConfig.DebugLevel.NONE; 229 break; 230 case APP_ONLY: 231 parcel.debugLevel = VirtualMachineAppConfig.DebugLevel.APP_ONLY; 232 break; 233 case FULL: 234 parcel.debugLevel = VirtualMachineAppConfig.DebugLevel.FULL; 235 break; 236 } 237 parcel.protectedVm = mProtectedVm; 238 parcel.memoryMib = mMemoryMib; 239 parcel.numCpus = mNumCpus; 240 parcel.cpuAffinity = mCpuAffinity; 241 // Don't allow apps to set task profiles ... at last for now. Also, don't forget to 242 // validate the string because these are appended to the cmdline argument. 243 parcel.taskProfiles = new String[0]; 244 return parcel; 245 } 246 247 /** A builder used to create a {@link VirtualMachineConfig}. */ 248 public static class Builder { 249 private Context mContext; 250 private String mPayloadConfigPath; 251 private DebugLevel mDebugLevel; 252 private boolean mProtectedVm; 253 private int mMemoryMib; 254 private int mNumCpus; 255 private String mCpuAffinity; 256 257 /** Creates a builder for the given context (APK), and the payload config file in APK. */ Builder(@onNull Context context, @NonNull String payloadConfigPath)258 public Builder(@NonNull Context context, @NonNull String payloadConfigPath) { 259 mContext = context; 260 mPayloadConfigPath = payloadConfigPath; 261 mDebugLevel = DebugLevel.NONE; 262 mProtectedVm = false; 263 mNumCpus = 1; 264 mCpuAffinity = null; 265 } 266 267 /** Sets the debug level */ debugLevel(DebugLevel debugLevel)268 public Builder debugLevel(DebugLevel debugLevel) { 269 mDebugLevel = debugLevel; 270 return this; 271 } 272 273 /** Sets whether to protect the VM memory from the host. Defaults to false. */ protectedVm(boolean protectedVm)274 public Builder protectedVm(boolean protectedVm) { 275 mProtectedVm = protectedVm; 276 return this; 277 } 278 279 /** 280 * Sets the amount of RAM to give the VM. If this is zero or negative then the default will 281 * be used. 282 */ memoryMib(int memoryMib)283 public Builder memoryMib(int memoryMib) { 284 mMemoryMib = memoryMib; 285 return this; 286 } 287 288 /** 289 * Sets the number of vCPUs in the VM. Defaults to 1. 290 */ numCpus(int num)291 public Builder numCpus(int num) { 292 mNumCpus = num; 293 return this; 294 } 295 296 /** 297 * Sets on which host CPUs the vCPUs can run. The format is a comma-separated list of CPUs 298 * or CPU ranges to run vCPUs on. e.g. "0,1-3,5" to choose host CPUs 0, 1, 2, 3, and 5. 299 * Or this can be a colon-separated list of assignments of vCPU to host CPU assignments. 300 * e.g. "0=0:1=1:2=2" to map vCPU 0 to host CPU 0, and so on. 301 */ cpuAffinity(String affinity)302 public Builder cpuAffinity(String affinity) { 303 mCpuAffinity = affinity; 304 return this; 305 } 306 307 /** Builds an immutable {@link VirtualMachineConfig} */ build()308 public @NonNull VirtualMachineConfig build() { 309 final String apkPath = mContext.getPackageCodePath(); 310 final String packageName = mContext.getPackageName(); 311 Signature[] certs; 312 try { 313 certs = 314 mContext.getPackageManager() 315 .getPackageInfo( 316 packageName, PackageManager.GET_SIGNING_CERTIFICATES) 317 .signingInfo 318 .getSigningCertificateHistory(); 319 } catch (PackageManager.NameNotFoundException e) { 320 // This cannot happen as `packageName` is from this app. 321 throw new RuntimeException(e); 322 } 323 324 final int availableCpus = Runtime.getRuntime().availableProcessors(); 325 if (mNumCpus < 1 || mNumCpus > availableCpus) { 326 throw new IllegalArgumentException("Number of vCPUs (" + mNumCpus + ") is out of " 327 + "range [1, " + availableCpus + "]"); 328 } 329 330 if (mCpuAffinity != null 331 && !Pattern.matches("[\\d]+(-[\\d]+)?(,[\\d]+(-[\\d]+)?)*", mCpuAffinity) 332 && !Pattern.matches("[\\d]+=[\\d]+(:[\\d]+=[\\d]+)*", mCpuAffinity)) { 333 throw new IllegalArgumentException("CPU affinity [" + mCpuAffinity + "]" 334 + " is invalid"); 335 } 336 337 if (mProtectedVm 338 && !HypervisorProperties.hypervisor_protected_vm_supported().orElse(false)) { 339 throw new UnsupportedOperationException( 340 "Protected VMs are not supported on this device."); 341 } 342 if (!mProtectedVm && !HypervisorProperties.hypervisor_vm_supported().orElse(false)) { 343 throw new UnsupportedOperationException( 344 "Unprotected VMs are not supported on this device."); 345 } 346 347 return new VirtualMachineConfig( 348 apkPath, certs, mPayloadConfigPath, mDebugLevel, mProtectedVm, mMemoryMib, 349 mNumCpus, mCpuAffinity); 350 } 351 } 352 } 353