1159 lines
43 KiB
Java
1159 lines
43 KiB
Java
/*
|
|
* 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.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 = {};
|
|
|
|
// These define the schema of the config file persisted on disk.
|
|
private static final int VERSION = 8;
|
|
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_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_VENDOR_DISK_IMAGE_PATH = "vendorDiskImagePath";
|
|
private static final String KEY_OS = "os";
|
|
private static final String KEY_EXTRA_APKS = "extraApks";
|
|
|
|
/** @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<String> 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;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
@Nullable private final File mVendorDiskImage;
|
|
|
|
/** OS name of the VM using payload binaries. */
|
|
@NonNull @OsName private final String mOs;
|
|
|
|
@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<String> extraApks,
|
|
@Nullable String payloadConfigPath,
|
|
@Nullable String payloadBinaryName,
|
|
@Nullable VirtualMachineCustomImageConfig customImageConfig,
|
|
@DebugLevel int debugLevel,
|
|
boolean protectedVm,
|
|
long memoryBytes,
|
|
@CpuTopology int cpuTopology,
|
|
long encryptedStorageBytes,
|
|
boolean vmOutputCaptured,
|
|
boolean vmConsoleInputSupported,
|
|
@Nullable File vendorDiskImage,
|
|
@NonNull @OsName String os) {
|
|
// 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;
|
|
mEncryptedStorageBytes = encryptedStorageBytes;
|
|
mVmOutputCaptured = vmOutputCaptured;
|
|
mVmConsoleInputSupported = vmConsoleInputSupported;
|
|
mVendorDiskImage = vendorDiskImage;
|
|
mOs = os;
|
|
}
|
|
|
|
/** 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));
|
|
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));
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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 (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);
|
|
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.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<String> 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/<ABI>} 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 the OS of the VM.
|
|
*
|
|
* @see Builder#setOs
|
|
* @hide
|
|
*/
|
|
@TestApi
|
|
@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
|
|
@NonNull
|
|
@OsName
|
|
public String getOs() {
|
|
return mOs;
|
|
}
|
|
|
|
/**
|
|
* 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.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);
|
|
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.protectedVm = this.mProtectedVm;
|
|
config.memoryMib = bytesToMebiBytes(mMemoryBytes);
|
|
config.cpuTopology = (byte) this.mCpuTopology;
|
|
config.devices = EMPTY_STRING_ARRAY;
|
|
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) {
|
|
VirtualMachineAppConfig.CustomConfig customConfig =
|
|
new VirtualMachineAppConfig.CustomConfig();
|
|
customConfig.devices = EMPTY_STRING_ARRAY;
|
|
try {
|
|
customConfig.vendorImage =
|
|
ParcelFileDescriptor.open(mVendorDiskImage, MODE_READ_ONLY);
|
|
} catch (FileNotFoundException e) {
|
|
throw new VirtualMachineException(
|
|
"Failed to open vendor disk image " + mVendorDiskImage.getAbsolutePath(),
|
|
e);
|
|
}
|
|
vsConfig.customConfig = customConfig;
|
|
}
|
|
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<String> 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;
|
|
private long mEncryptedStorageBytes;
|
|
private boolean mVmOutputCaptured = false;
|
|
private boolean mVmConsoleInputSupported = false;
|
|
@Nullable private File mVendorDiskImage;
|
|
@NonNull @OsName private String mOs = DEFAULT_OS;
|
|
|
|
/**
|
|
* 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");
|
|
}
|
|
|
|
return new VirtualMachineConfig(
|
|
packageName,
|
|
apkPath,
|
|
mExtraApks,
|
|
mPayloadConfigPath,
|
|
mPayloadBinaryName,
|
|
mCustomImageConfig,
|
|
mDebugLevel,
|
|
mProtectedVm,
|
|
mMemoryBytes,
|
|
mCpuTopology,
|
|
mEncryptedStorageBytes,
|
|
mVmOutputCaptured,
|
|
mVmConsoleInputSupported,
|
|
mVendorDiskImage,
|
|
mOs);
|
|
}
|
|
|
|
/**
|
|
* 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/<ABI>} directory of the APK.
|
|
*
|
|
* <p>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}.
|
|
*
|
|
* <p>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}.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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}.
|
|
*
|
|
* <p>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 size (in bytes) of encrypted storage available to the VM. If not set, no
|
|
* encrypted storage is provided.
|
|
*
|
|
* <p>The storage is encrypted with a key deterministically derived from the VM identity
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>Note - There is no integrity guarantee or rollback protection on the storage in case
|
|
* the encrypted data is modified.
|
|
*
|
|
* <p>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}.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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.
|
|
*
|
|
* <p>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}.
|
|
*
|
|
* <p>Setting this as {@code true} will allow the app to directly write into {@linkplain
|
|
* VirtualMachine#getConsoleInput console input}.
|
|
*
|
|
* <p>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 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"}.
|
|
*
|
|
* <p>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;
|
|
}
|
|
}
|
|
}
|