blob: c200d00857f8359a4add480c025ee56180980a9d [file] [log] [blame]
/*
* 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 android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_CHANGED;
import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_INVALID_CONFIG;
import static android.system.virtualmachine.VirtualMachineCallback.ERROR_PAYLOAD_VERIFICATION_FAILED;
import static android.system.virtualmachine.VirtualMachineCallback.ERROR_UNKNOWN;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_CRASH;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_ERROR;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_HANGUP;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_INFRASTRUCTURE_ERROR;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_KILLED;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_REBOOT;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_SHUTDOWN;
import static android.system.virtualmachine.VirtualMachineCallback.STOP_REASON_UNKNOWN;
import static java.util.Objects.requireNonNull;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.content.Context;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.system.virtualizationcommon.ErrorCode;
import android.system.virtualizationservice.DeathReason;
import android.system.virtualizationservice.IVirtualMachine;
import android.system.virtualizationservice.IVirtualMachineCallback;
import android.system.virtualizationservice.IVirtualizationService;
import android.system.virtualizationservice.PartitionType;
import android.system.virtualizationservice.VirtualMachineAppConfig;
import android.system.virtualizationservice.VirtualMachineState;
import android.util.JsonReader;
import com.android.internal.annotations.GuardedBy;
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.InputStreamReader;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.nio.channels.FileChannel;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.zip.ZipFile;
/**
* Represents an VM instance, with its own configuration and state. Instances are persistent and are
* created or retrieved via {@link VirtualMachineManager}.
* <p>
* The {@link #run} method actually starts up the VM and allows the payload code to execute. It
* will continue until it exits or {@link #stop} is called. Updates on the state of the VM can
* be received using {@link #setCallback}. The app can communicate with the VM using
* {@link #connectToVsockServer} or {@link #connectVsock}.
*
* @hide
*/
public class VirtualMachine implements AutoCloseable {
/** Map from context to a map of all that context's VMs by name. */
@GuardedBy("sCreateLock")
private static final Map<Context, Map<String, WeakReference<VirtualMachine>>> sInstances =
new WeakHashMap<>();
/** Name of the directory under the files directory where all VMs created for the app exist. */
private static final String VM_DIR = "vm";
/** Name of the persisted config file for a VM. */
private static final String CONFIG_FILE = "config.xml";
/** Name of the instance image file for a VM. (Not implemented) */
private static final String INSTANCE_IMAGE_FILE = "instance.img";
/** Name of the idsig file for a VM */
private static final String IDSIG_FILE = "idsig";
/** Name of the idsig files for extra APKs. */
private static final String EXTRA_IDSIG_FILE_PREFIX = "extra_idsig_";
/** Name of the virtualization service. */
private static final String SERVICE_NAME = "android.system.virtualizationservice";
/** The permission needed to create or run a virtual machine. */
public static final String MANAGE_VIRTUAL_MACHINE_PERMISSION =
"android.permission.MANAGE_VIRTUAL_MACHINE";
/**
* The permission needed to create a virtual machine with more advanced configuration options.
*/
public static final String USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION =
"android.permission.USE_CUSTOM_VIRTUAL_MACHINE";
/**
* Status of a virtual machine
*
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = "STATUS_", value = {
STATUS_STOPPED,
STATUS_RUNNING,
STATUS_DELETED
})
public @interface Status {}
/** The virtual machine has just been created, or {@link #stop()} was called on it. */
public static final int STATUS_STOPPED = 0;
/** The virtual machine is running. */
public static final int STATUS_RUNNING = 1;
/**
* The virtual machine has been deleted. This is an irreversible state. Once a virtual machine
* is deleted all its secrets are permanently lost, and it cannot be run. A new virtual machine
* with the same name and config may be created, with new and different secrets.
*/
public static final int STATUS_DELETED = 2;
/** The package which owns this VM. */
@NonNull private final String mPackageName;
/** Name of this VM within the package. The name should be unique in the package. */
@NonNull private final String mName;
/**
* Path to the directory containing all the files related to this VM.
*/
@NonNull private final File mVmRootPath;
/**
* Path to the config file for this VM. The config file is where the configuration is persisted.
*/
@NonNull private final File mConfigFilePath;
/** Path to the instance image file for this VM. */
@NonNull private final File mInstanceFilePath;
/** Path to the idsig file for this VM. */
@NonNull private final File mIdsigFilePath;
private static class ExtraApkSpec {
public final File apk;
public final File idsig;
ExtraApkSpec(File apk, File idsig) {
this.apk = apk;
this.idsig = idsig;
}
}
/**
* Unmodifiable list of extra apks. Apks are specified by the vm config, and corresponding
* idsigs are to be generated.
*/
@NonNull private final List<ExtraApkSpec> mExtraApks;
/** Size of the instance image. 10 MB. */
private static final long INSTANCE_FILE_SIZE = 10 * 1024 * 1024;
// A note on lock ordering:
// You can take mLock while holding sCreateLock, but not vice versa.
// We never take any other lock while holding mCallbackLock; therefore you can
// take mCallbackLock while holding any other lock.
/**
* A lock used to synchronize the creation of virtual machines. It protects
* {@link #sInstances}, but is also held throughout VM creation / retrieval / deletion, to
* prevent these actions racing with each other.
*/
static final Object sCreateLock = new Object();
/** Lock protecting our mutable state (other than callbacks). */
private final Object mLock = new Object();
/** Lock protecting callbacks. */
private final Object mCallbackLock = new Object();
/** The configuration that is currently associated with this VM. */
@GuardedBy("mLock")
@NonNull
private VirtualMachineConfig mConfig;
/** Handle to the "running" VM. */
@GuardedBy("mLock")
@Nullable
private IVirtualMachine mVirtualMachine;
@GuardedBy("mLock")
@Nullable
private ParcelFileDescriptor mConsoleReader;
@GuardedBy("mLock")
@Nullable
private ParcelFileDescriptor mConsoleWriter;
@GuardedBy("mLock")
@Nullable
private ParcelFileDescriptor mLogReader;
@GuardedBy("mLock")
@Nullable
private ParcelFileDescriptor mLogWriter;
/** The registered callback */
@GuardedBy("mCallbackLock")
@Nullable
private VirtualMachineCallback mCallback;
/** The executor on which the callback will be executed */
@GuardedBy("mCallbackLock")
@Nullable
private Executor mCallbackExecutor;
static {
System.loadLibrary("virtualmachine_jni");
}
private VirtualMachine(
@NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config)
throws VirtualMachineException {
mPackageName = context.getPackageName();
mName = requireNonNull(name, "Name must not be null");
mConfig = requireNonNull(config, "Config must not be null");
File thisVmDir = getVmDir(context, mName);
mVmRootPath = thisVmDir;
mConfigFilePath = new File(thisVmDir, CONFIG_FILE);
mInstanceFilePath = new File(thisVmDir, INSTANCE_IMAGE_FILE);
mIdsigFilePath = new File(thisVmDir, IDSIG_FILE);
mExtraApks = setupExtraApks(context, config, thisVmDir);
}
@GuardedBy("sCreateLock")
@NonNull
private static Map<String, WeakReference<VirtualMachine>> getInstancesMap(Context context) {
return sInstances.computeIfAbsent(context, unused -> new HashMap<>());
}
/**
* Builds a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
* with the given name.
*
* <p>The new virtual machine will be in the same state as the descriptor indicates.
*
* <p>Once a virtual machine is imported it is persisted until it is deleted by calling {@link
* #delete}. The imported virtual machine is in {@link #STATUS_STOPPED} state. To run the VM,
* call {@link #run}.
*/
@GuardedBy("sCreateLock")
@NonNull
static VirtualMachine fromDescriptor(
@NonNull Context context,
@NonNull String name,
@NonNull VirtualMachineDescriptor vmDescriptor)
throws VirtualMachineException {
VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
File vmDir = createVmDir(context, name);
try {
VirtualMachine vm = new VirtualMachine(context, name, config);
config.serialize(vm.mConfigFilePath);
try {
vm.mInstanceFilePath.createNewFile();
} catch (IOException e) {
throw new VirtualMachineException("failed to create instance image", e);
}
vm.importInstanceFrom(vmDescriptor.getInstanceImgFd());
getInstancesMap(context).put(name, new WeakReference<>(vm));
return vm;
} catch (VirtualMachineException | RuntimeException e) {
// If anything goes wrong, delete any files created so far and the VM's directory
try {
deleteRecursively(vmDir);
} catch (IOException innerException) {
e.addSuppressed(innerException);
}
throw e;
}
}
/**
* Creates a virtual machine with the given name and config. Once a virtual machine is created
* it is persisted until it is deleted by calling {@link #delete}. The created virtual machine
* is in {@link #STATUS_STOPPED} state. To run the VM, call {@link #run}.
*/
@GuardedBy("sCreateLock")
@NonNull
static VirtualMachine create(
@NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config)
throws VirtualMachineException {
File vmDir = createVmDir(context, name);
try {
VirtualMachine vm = new VirtualMachine(context, name, config);
config.serialize(vm.mConfigFilePath);
try {
vm.mInstanceFilePath.createNewFile();
} catch (IOException e) {
throw new VirtualMachineException("failed to create instance image", e);
}
IVirtualizationService service =
IVirtualizationService.Stub.asInterface(
ServiceManager.waitForService(SERVICE_NAME));
try {
service.initializeWritablePartition(
ParcelFileDescriptor.open(vm.mInstanceFilePath, MODE_READ_WRITE),
INSTANCE_FILE_SIZE,
PartitionType.ANDROID_VM_INSTANCE);
} catch (FileNotFoundException e) {
throw new VirtualMachineException("instance image missing", e);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
} catch (ServiceSpecificException | IllegalArgumentException e) {
throw new VirtualMachineException("failed to create instance partition", e);
}
getInstancesMap(context).put(name, new WeakReference<>(vm));
return vm;
} catch (VirtualMachineException | RuntimeException e) {
// If anything goes wrong, delete any files created so far and the VM's directory
try {
deleteRecursively(vmDir);
} catch (IOException innerException) {
e.addSuppressed(innerException);
}
throw e;
}
}
/** Loads a virtual machine that is already created before. */
@GuardedBy("sCreateLock")
@Nullable
static VirtualMachine load(
@NonNull Context context, @NonNull String name) throws VirtualMachineException {
File thisVmDir = getVmDir(context, name);
if (!thisVmDir.exists()) {
// The VM doesn't exist.
return null;
}
File configFilePath = new File(thisVmDir, CONFIG_FILE);
VirtualMachineConfig config = VirtualMachineConfig.from(configFilePath);
Map<String, WeakReference<VirtualMachine>> instancesMap = getInstancesMap(context);
VirtualMachine vm = null;
if (instancesMap.containsKey(name)) {
vm = instancesMap.get(name).get();
}
if (vm == null) {
vm = new VirtualMachine(context, name, config);
}
if (!vm.mInstanceFilePath.exists()) {
throw new VirtualMachineException("instance image missing");
}
instancesMap.put(name, new WeakReference<>(vm));
return vm;
}
@GuardedBy("sCreateLock")
static void delete(Context context, String name) throws VirtualMachineException {
Map<String, WeakReference<VirtualMachine>> instancesMap = sInstances.get(context);
VirtualMachine vm;
if (instancesMap != null && instancesMap.containsKey(name)) {
vm = instancesMap.get(name).get();
} else {
vm = null;
}
if (vm != null) {
synchronized (vm.mLock) {
vm.checkStopped();
}
}
try {
deleteRecursively(getVmDir(context, name));
} catch (IOException e) {
throw new VirtualMachineException(e);
}
if (instancesMap != null) instancesMap.remove(name);
}
@GuardedBy("sCreateLock")
@NonNull
private static File createVmDir(@NonNull Context context, @NonNull String name)
throws VirtualMachineException {
File vmDir = getVmDir(context, name);
try {
// We don't need to undo this even if VM creation fails.
Files.createDirectories(vmDir.getParentFile().toPath());
// The checking of the existence of this directory and the creation of it is done
// atomically. If the directory already exists (i.e. the VM with the same name was
// already created), FileAlreadyExistsException is thrown.
Files.createDirectory(vmDir.toPath());
} catch (FileAlreadyExistsException e) {
throw new VirtualMachineException("virtual machine already exists", e);
} catch (IOException e) {
throw new VirtualMachineException("failed to create directory for VM", e);
}
return vmDir;
}
@NonNull
private static File getVmDir(Context context, String name) {
File vmRoot = new File(context.getDataDir(), VM_DIR);
return new File(vmRoot, name);
}
/**
* Returns the name of this virtual machine. The name is unique in the package and can't be
* changed.
*
* @hide
*/
@NonNull
public String getName() {
return mName;
}
/**
* Returns the currently selected config of this virtual machine. There can be multiple virtual
* machines sharing the same config. Even in that case, the virtual machines are completely
* isolated from each other; one cannot share its secret to another virtual machine even if they
* share the same config. It is also possible that a virtual machine can switch its config,
* which can be done by calling {@link #setConfig(VirtualMachineConfig)}.
*
* @hide
*/
@NonNull
public VirtualMachineConfig getConfig() {
synchronized (mLock) {
return mConfig;
}
}
/**
* Returns the current status of this virtual machine.
*
* @hide
*/
@Status
public int getStatus() {
IVirtualMachine virtualMachine;
synchronized (mLock) {
virtualMachine = mVirtualMachine;
}
if (virtualMachine == null) {
return mVmRootPath.exists() ? STATUS_STOPPED : STATUS_DELETED;
} else {
try {
return stateToStatus(virtualMachine.getState());
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
}
private int stateToStatus(@VirtualMachineState int state) {
switch (state) {
case VirtualMachineState.STARTING:
case VirtualMachineState.STARTED:
case VirtualMachineState.READY:
case VirtualMachineState.FINISHED:
return STATUS_RUNNING;
case VirtualMachineState.NOT_STARTED:
case VirtualMachineState.DEAD:
default:
return STATUS_STOPPED;
}
}
// Throw an appropriate exception if we have a running VM, or the VM has been deleted.
@GuardedBy("mLock")
private void checkStopped() throws VirtualMachineException {
if (!mVmRootPath.exists()) {
throw new VirtualMachineException("VM has been deleted");
}
if (mVirtualMachine == null) {
return;
}
try {
if (stateToStatus(mVirtualMachine.getState()) != STATUS_STOPPED) {
throw new VirtualMachineException("VM is not in stopped state");
}
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
// If we have an IVirtualMachine in the running state return it, otherwise throw.
@GuardedBy("mLock")
private IVirtualMachine getRunningVm() throws VirtualMachineException {
try {
if (mVirtualMachine != null
&& stateToStatus(mVirtualMachine.getState()) == STATUS_RUNNING) {
return mVirtualMachine;
} else {
if (!mVmRootPath.exists()) {
throw new VirtualMachineException("VM has been deleted");
} else {
throw new VirtualMachineException("VM is not in running state");
}
}
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
/**
* Registers the callback object to get events from the virtual machine. If a callback was
* already registered, it is replaced with the new one.
*
* @hide
*/
public void setCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull VirtualMachineCallback callback) {
synchronized (mCallbackLock) {
mCallback = callback;
mCallbackExecutor = executor;
}
}
/**
* Clears the currently registered callback.
*
* @hide
*/
public void clearCallback() {
synchronized (mCallbackLock) {
mCallback = null;
mCallbackExecutor = null;
}
}
/** Executes a callback on the callback executor. */
private void executeCallback(Consumer<VirtualMachineCallback> fn) {
final VirtualMachineCallback callback;
final Executor executor;
synchronized (mCallbackLock) {
callback = mCallback;
executor = mCallbackExecutor;
}
if (callback == null || executor == null) {
return;
}
final long restoreToken = Binder.clearCallingIdentity();
try {
executor.execute(() -> fn.accept(callback));
} finally {
Binder.restoreCallingIdentity(restoreToken);
}
}
/**
* Runs this virtual machine. The returning of this method however doesn't mean that the VM has
* actually started running or the OS has booted there. Such events can be notified by
* registering a callback using {@link #setCallback(Executor, VirtualMachineCallback)} before
* calling {@code run()}.
*
* @throws VirtualMachineException if the virtual machine is not stopped or could not be
* started.
* @hide
*/
@RequiresPermission(MANAGE_VIRTUAL_MACHINE_PERMISSION)
public void run() throws VirtualMachineException {
synchronized (mLock) {
checkStopped();
try {
mIdsigFilePath.createNewFile();
for (ExtraApkSpec extraApk : mExtraApks) {
extraApk.idsig.createNewFile();
}
} catch (IOException e) {
// If the file already exists, exception is not thrown.
throw new VirtualMachineException("failed to create idsig file", e);
}
IVirtualizationService service =
IVirtualizationService.Stub.asInterface(
ServiceManager.waitForService(SERVICE_NAME));
try {
createVmPipes();
VirtualMachineAppConfig appConfig = getConfig().toVsConfig();
appConfig.name = mName;
// Fill the idsig file by hashing the apk
service.createOrUpdateIdsigFile(
appConfig.apk, ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_WRITE));
for (ExtraApkSpec extraApk : mExtraApks) {
service.createOrUpdateIdsigFile(
ParcelFileDescriptor.open(extraApk.apk, MODE_READ_ONLY),
ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_WRITE));
}
// Re-open idsig file in read-only mode
appConfig.idsig = ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_ONLY);
appConfig.instanceImage = ParcelFileDescriptor.open(mInstanceFilePath,
MODE_READ_WRITE);
List<ParcelFileDescriptor> extraIdsigs = new ArrayList<>();
for (ExtraApkSpec extraApk : mExtraApks) {
extraIdsigs.add(ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_ONLY));
}
appConfig.extraIdsigs = extraIdsigs;
android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
android.system.virtualizationservice.VirtualMachineConfig.appConfig(
appConfig);
// The VM should only be observed to die once
AtomicBoolean onDiedCalled = new AtomicBoolean(false);
IBinder.DeathRecipient deathRecipient = () -> {
if (onDiedCalled.compareAndSet(false, true)) {
executeCallback((cb) -> cb.onStopped(VirtualMachine.this,
VirtualMachineCallback.STOP_REASON_VIRTUALIZATION_SERVICE_DIED));
}
};
mVirtualMachine = service.createVm(vmConfigParcel, mConsoleWriter, mLogWriter);
mVirtualMachine.registerCallback(
new IVirtualMachineCallback.Stub() {
@Override
public void onPayloadStarted(int cid) {
executeCallback((cb) -> cb.onPayloadStarted(VirtualMachine.this));
}
@Override
public void onPayloadStdio(int cid, ParcelFileDescriptor stream) {
executeCallback(
(cb) -> cb.onPayloadStdio(VirtualMachine.this, stream));
}
@Override
public void onPayloadReady(int cid) {
executeCallback((cb) -> cb.onPayloadReady(VirtualMachine.this));
}
@Override
public void onPayloadFinished(int cid, int exitCode) {
executeCallback(
(cb) ->
cb.onPayloadFinished(
VirtualMachine.this, exitCode));
}
@Override
public void onError(int cid, int errorCode, String message) {
int translatedError = getTranslatedError(errorCode);
executeCallback(
(cb) ->
cb.onError(
VirtualMachine.this,
translatedError,
message));
}
@Override
public void onDied(int cid, int reason) {
service.asBinder().unlinkToDeath(deathRecipient, 0);
int translatedReason = getTranslatedReason(reason);
if (onDiedCalled.compareAndSet(false, true)) {
executeCallback(
(cb) ->
cb.onStopped(
VirtualMachine.this, translatedReason));
}
}
@Override
public void onRamdump(int cid, ParcelFileDescriptor ramdump) {
executeCallback((cb) -> cb.onRamdump(VirtualMachine.this, ramdump));
}
});
service.asBinder().linkToDeath(deathRecipient, 0);
mVirtualMachine.start();
} catch (IOException | IllegalStateException | ServiceSpecificException e) {
throw new VirtualMachineException(e);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
}
@GuardedBy("mLock")
private void createVmPipes() throws VirtualMachineException {
try {
if (mConsoleReader == null || mConsoleWriter == null) {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
mConsoleReader = pipe[0];
mConsoleWriter = pipe[1];
}
if (mLogReader == null || mLogWriter == null) {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
mLogReader = pipe[0];
mLogWriter = pipe[1];
}
} catch (IOException e) {
throw new VirtualMachineException("Failed to create stream for VM", e);
}
}
/**
* Returns the stream object representing the console output from the virtual machine.
*
* @throws VirtualMachineException if the stream could not be created.
* @hide
*/
@NonNull
public InputStream getConsoleOutput() throws VirtualMachineException {
synchronized (mLock) {
createVmPipes();
return new FileInputStream(mConsoleReader.getFileDescriptor());
}
}
/**
* Returns the stream object representing the log output from the virtual machine.
*
* @throws VirtualMachineException if the stream could not be created.
* @hide
*/
@NonNull
public InputStream getLogOutput() throws VirtualMachineException {
synchronized (mLock) {
createVmPipes();
return new FileInputStream(mLogReader.getFileDescriptor());
}
}
/**
* Stops this virtual machine. Stopping a virtual machine is like pulling the plug on a real
* computer; the machine halts immediately. Software running on the virtual machine is not
* notified of the event. A stopped virtual machine can be re-started by calling {@link
* #run()}.
*
* @throws VirtualMachineException if the virtual machine could not be stopped.
* @hide
*/
public void stop() throws VirtualMachineException {
synchronized (mLock) {
if (mVirtualMachine == null) {
throw new VirtualMachineException("VM is not running");
}
try {
mVirtualMachine.stop();
mVirtualMachine = null;
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
} catch (ServiceSpecificException e) {
throw new VirtualMachineException(e);
}
}
}
/**
* Stops this virtual machine. See {@link #stop()}.
*
* @throws VirtualMachineException if the virtual machine could not be stopped.
* @hide
*/
@Override
public void close() throws VirtualMachineException {
stop();
}
private static void deleteRecursively(File dir) throws IOException {
// Note: This doesn't follow symlinks, which is important. Instead they are just deleted
// (and Files.delete deletes the link not the target).
Files.walkFileTree(dir.toPath(), new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
// Directory is deleted after we've visited (deleted) all its contents, so it
// should be empty by now.
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}
/**
* Returns the CID of this virtual machine, if it is running.
*
* @throws VirtualMachineException if the virtual machine is not running.
* @hide
*/
public int getCid() throws VirtualMachineException {
synchronized (mLock) {
try {
return getRunningVm().getCid();
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
}
/**
* Changes the config of this virtual machine to a new one. This can be used to adjust things
* like the number of CPU and size of the RAM, depending on the situation (e.g. the size of the
* application to run on the virtual machine, etc.)
*
* The new config must be {@link VirtualMachineConfig#isCompatibleWith compatible with} the
* existing config.
*
* @return the old config
* @throws VirtualMachineException if the virtual machine is not stopped, or the new config is
* incompatible.
* @hide
*/
@NonNull
public VirtualMachineConfig setConfig(@NonNull VirtualMachineConfig newConfig)
throws VirtualMachineException {
synchronized (mLock) {
VirtualMachineConfig oldConfig = mConfig;
if (!oldConfig.isCompatibleWith(newConfig)) {
throw new VirtualMachineException("incompatible config");
}
checkStopped();
newConfig.serialize(mConfigFilePath);
mConfig = newConfig;
return oldConfig;
}
}
@Nullable
private static native IBinder nativeConnectToVsockServer(IBinder vmBinder, int port);
/**
* Connect to a VM's binder service via vsock and return the root IBinder object. Guest VMs are
* expected to set up vsock servers in their payload. After the host app receives the {@link
* VirtualMachineCallback#onPayloadReady(VirtualMachine)}, it can use this method to
* establish a connection to the guest VM.
*
* @throws VirtualMachineException if the virtual machine is not running or the connection
* failed.
* @hide
*/
@NonNull
public IBinder connectToVsockServer(int port) throws VirtualMachineException {
synchronized (mLock) {
IBinder iBinder = nativeConnectToVsockServer(getRunningVm().asBinder(), port);
if (iBinder == null) {
throw new VirtualMachineException("Failed to connect to vsock server");
}
return iBinder;
}
}
/**
* Opens a vsock connection to the VM on the given port.
*
* @throws VirtualMachineException if connecting fails.
* @hide
*/
@NonNull
public ParcelFileDescriptor connectVsock(int port) throws VirtualMachineException {
synchronized (mLock) {
try {
return getRunningVm().connectVsock(port);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
} catch (ServiceSpecificException e) {
throw new VirtualMachineException(e);
}
}
}
/**
* Captures the current state of the VM in a {@link VirtualMachineDescriptor} instance. The VM
* needs to be stopped to avoid inconsistency in its state representation.
*
* @return a {@link VirtualMachineDescriptor} instance that represents the VM's state.
* @throws VirtualMachineException if the virtual machine is not stopped, or the state could not
* be captured.
* @hide
*/
@NonNull
public VirtualMachineDescriptor toDescriptor() throws VirtualMachineException {
synchronized (mLock) {
checkStopped();
}
try {
return new VirtualMachineDescriptor(
ParcelFileDescriptor.open(mConfigFilePath, MODE_READ_ONLY),
ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_ONLY));
} catch (IOException e) {
throw new VirtualMachineException(e);
}
}
@VirtualMachineCallback.ErrorCode
private int getTranslatedError(int reason) {
switch (reason) {
case ErrorCode.PAYLOAD_VERIFICATION_FAILED:
return ERROR_PAYLOAD_VERIFICATION_FAILED;
case ErrorCode.PAYLOAD_CHANGED:
return ERROR_PAYLOAD_CHANGED;
case ErrorCode.PAYLOAD_CONFIG_INVALID:
return ERROR_PAYLOAD_INVALID_CONFIG;
default:
return ERROR_UNKNOWN;
}
}
@VirtualMachineCallback.StopReason
private int getTranslatedReason(int reason) {
switch (reason) {
case DeathReason.INFRASTRUCTURE_ERROR:
return STOP_REASON_INFRASTRUCTURE_ERROR;
case DeathReason.KILLED:
return STOP_REASON_KILLED;
case DeathReason.SHUTDOWN:
return STOP_REASON_SHUTDOWN;
case DeathReason.ERROR:
return STOP_REASON_ERROR;
case DeathReason.REBOOT:
return STOP_REASON_REBOOT;
case DeathReason.CRASH:
return STOP_REASON_CRASH;
case DeathReason.PVM_FIRMWARE_PUBLIC_KEY_MISMATCH:
return STOP_REASON_PVM_FIRMWARE_PUBLIC_KEY_MISMATCH;
case DeathReason.PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED:
return STOP_REASON_PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED;
case DeathReason.BOOTLOADER_PUBLIC_KEY_MISMATCH:
return STOP_REASON_BOOTLOADER_PUBLIC_KEY_MISMATCH;
case DeathReason.BOOTLOADER_INSTANCE_IMAGE_CHANGED:
return STOP_REASON_BOOTLOADER_INSTANCE_IMAGE_CHANGED;
case DeathReason.MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE:
return STOP_REASON_MICRODROID_FAILED_TO_CONNECT_TO_VIRTUALIZATION_SERVICE;
case DeathReason.MICRODROID_PAYLOAD_HAS_CHANGED:
return STOP_REASON_MICRODROID_PAYLOAD_HAS_CHANGED;
case DeathReason.MICRODROID_PAYLOAD_VERIFICATION_FAILED:
return STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED;
case DeathReason.MICRODROID_INVALID_PAYLOAD_CONFIG:
return STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG;
case DeathReason.MICRODROID_UNKNOWN_RUNTIME_ERROR:
return STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR;
case DeathReason.HANGUP:
return STOP_REASON_HANGUP;
default:
return STOP_REASON_UNKNOWN;
}
}
@Override
public String toString() {
VirtualMachineConfig config = getConfig();
String payloadConfigPath = config.getPayloadConfigPath();
String payloadBinaryPath = config.getPayloadBinaryPath();
StringBuilder result = new StringBuilder();
result.append("VirtualMachine(")
.append("name:")
.append(getName())
.append(", ");
if (payloadBinaryPath != null) {
result.append("payload:")
.append(payloadBinaryPath)
.append(", ");
}
if (payloadConfigPath != null) {
result.append("config:")
.append(payloadConfigPath)
.append(", ");
}
result.append("package: ")
.append(mPackageName)
.append(")");
return result.toString();
}
private static List<String> parseExtraApkListFromPayloadConfig(JsonReader reader)
throws VirtualMachineException {
/*
* JSON schema from packages/modules/Virtualization/microdroid/payload/config/src/lib.rs:
*
* <p>{ "extra_apks": [ { "path": "/system/app/foo.apk", }, ... ], ... }
*/
try {
List<String> apks = new ArrayList<>();
reader.beginObject();
while (reader.hasNext()) {
if (reader.nextName().equals("extra_apks")) {
reader.beginArray();
while (reader.hasNext()) {
reader.beginObject();
String name = reader.nextName();
if (name.equals("path")) {
apks.add(reader.nextString());
} else {
reader.skipValue();
}
reader.endObject();
}
reader.endArray();
} else {
reader.skipValue();
}
}
reader.endObject();
return apks;
} catch (IOException e) {
throw new VirtualMachineException(e);
}
}
/**
* Reads the payload config inside the application, parses extra APK information, and then
* creates corresponding idsig file paths.
*/
private static List<ExtraApkSpec> setupExtraApks(
@NonNull Context context, @NonNull VirtualMachineConfig config, @NonNull File vmDir)
throws VirtualMachineException {
String configPath = config.getPayloadConfigPath();
if (configPath == null) {
return Collections.emptyList();
}
try {
ZipFile zipFile = new ZipFile(context.getPackageCodePath());
InputStream inputStream =
zipFile.getInputStream(zipFile.getEntry(configPath));
List<String> apkList =
parseExtraApkListFromPayloadConfig(
new JsonReader(new InputStreamReader(inputStream)));
List<ExtraApkSpec> extraApks = new ArrayList<>();
for (int i = 0; i < apkList.size(); ++i) {
extraApks.add(
new ExtraApkSpec(
new File(apkList.get(i)),
new File(vmDir, EXTRA_IDSIG_FILE_PREFIX + i)));
}
return Collections.unmodifiableList(extraApks);
} catch (IOException e) {
throw new VirtualMachineException("Couldn't parse extra apks from the vm config", e);
}
}
private void importInstanceFrom(@NonNull ParcelFileDescriptor instanceFd)
throws VirtualMachineException {
try (FileChannel instance = new FileOutputStream(mInstanceFilePath).getChannel();
FileChannel instanceInput = new AutoCloseInputStream(instanceFd).getChannel()) {
instance.transferFrom(instanceInput, /*position=*/ 0, instanceInput.size());
} catch (IOException e) {
throw new VirtualMachineException("failed to transfer instance image", e);
}
}
}