• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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