Merge "Microdroid: Map a dm-crypt dev on (virtio-blk)disk"
diff --git a/OWNERS b/OWNERS
index ecd24ed..310add7 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,5 +1,7 @@
# Welcome to Android KVM!
#
+# Bug component: 867125
+#
# If you are not a member of the project please send review requests
# to one of those listed below.
dbrazdil@google.com
diff --git a/apex/Android.bp b/apex/Android.bp
index e0ca9bf..4e64e50 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -15,20 +15,58 @@
"microdroid_vendor_boot",
]
-apex {
- name: "com.android.virt",
+soong_config_module_type {
+ name: "virt_apex",
+ module_type: "apex",
+ config_namespace: "ANDROID",
+ bool_variables: ["avf_enabled"],
+ properties: ["defaults"],
+}
+virt_apex {
+ name: "com.android.virt",
+ soong_config_variables: {
+ avf_enabled: {
+ defaults: ["com.android.virt_avf_enabled"],
+ conditions_default: {
+ defaults: ["com.android.virt_avf_disabled"],
+ },
+ },
+ },
+}
+
+apex_defaults {
+ name: "com.android.virt_common",
// TODO(jiyong): make it updatable
updatable: false,
future_updatable: true,
platform_apis: true,
- system_ext_specific: true,
-
manifest: "manifest.json",
key: "com.android.virt.key",
certificate: ":com.android.virt.certificate",
+
+ apps: [
+ "android.system.virtualmachine.res",
+ ],
+
+ file_contexts: ":com.android.virt-file_contexts",
+ canned_fs_config: "canned_fs_config",
+
+ bootclasspath_fragments: [
+ "com.android.virt-bootclasspath-fragment",
+ ],
+ jni_libs: [
+ "libvirtualmachine_jni",
+ ],
+}
+
+apex_defaults {
+ name: "com.android.virt_avf_enabled",
+
+ defaults: ["com.android.virt_common"],
+
custom_sign_tool: "sign_virt_apex",
// crosvm and virtualizationservice are only enabled for 64-bit targets on device
@@ -52,17 +90,9 @@
"fd_server",
"vm",
],
- java_libs: [
- "android.system.virtualmachine",
- ],
- jni_libs: [
- "libvirtualmachine_jni",
- ],
- apps: [
- "android.system.virtualmachine.res",
- ],
prebuilts: [
"com.android.virt.init.rc",
+ "features_com.android.virt.xml",
"microdroid_initrd_app_debuggable",
"microdroid_initrd_full_debuggable",
"microdroid_initrd_normal",
@@ -71,13 +101,17 @@
"microdroid_bootloader.avbpubkey",
"microdroid_kernel",
],
- file_contexts: ":com.android.virt-file_contexts",
- canned_fs_config: "canned_fs_config",
host_required: [
"vm_shell",
],
}
+apex_defaults {
+ name: "com.android.virt_avf_disabled",
+
+ defaults: ["com.android.virt_common"],
+}
+
apex_key {
name: "com.android.virt.key",
public_key: "com.android.virt.avbpubkey",
@@ -174,3 +208,43 @@
},
},
}
+
+// Encapsulate the contributions made by the com.android.virt to the bootclasspath.
+bootclasspath_fragment {
+ name: "com.android.virt-bootclasspath-fragment",
+ contents: ["framework-virtualization"],
+ apex_available: ["com.android.virt"],
+
+ // The bootclasspath_fragments that provide APIs on which this depends.
+ fragments: [
+ {
+ apex: "com.android.art",
+ module: "art-bootclasspath-fragment",
+ },
+ ],
+
+ // Additional stubs libraries that this fragment's contents use which are
+ // not provided by another bootclasspath_fragment.
+ additional_stubs: [
+ "android-non-updatable",
+ ],
+
+ hidden_api: {
+
+ // This module does not contain any split packages.
+ split_packages: [],
+
+ // The following packages and all their subpackages currently only
+ // contain classes from this bootclasspath_fragment. Listing a package
+ // here won't prevent other bootclasspath modules from adding classes in
+ // any of those packages but it will prevent them from adding those
+ // classes into an API surface, e.g. public, system, etc.. Doing so will
+ // result in a build failure due to inconsistent flags.
+ package_prefixes: [
+ "android.system.virtualmachine",
+ "android.system.virtualizationservice",
+ // android.sysprop.*, renamed by jarjar
+ "com.android.system.virtualmachine.sysprop",
+ ],
+ },
+}
diff --git a/apex/permissions/Android.bp b/apex/permissions/Android.bp
new file mode 100644
index 0000000..0c925ce
--- /dev/null
+++ b/apex/permissions/Android.bp
@@ -0,0 +1,24 @@
+//
+// Copyright (C) 2022 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+prebuilt_etc {
+ name: "features_com.android.virt.xml",
+ sub_dir: "permissions",
+ src: "features_com.android.virt.xml",
+}
diff --git a/apex/permissions/features_com.android.virt.xml b/apex/permissions/features_com.android.virt.xml
new file mode 100644
index 0000000..d2b32e6
--- /dev/null
+++ b/apex/permissions/features_com.android.virt.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 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.
+ -->
+<permissions>
+ <feature name="android.software.virtualization_framework" />
+</permissions>
diff --git a/apex/product_packages.mk b/apex/product_packages.mk
index 5bc0044..4293c80 100644
--- a/apex/product_packages.mk
+++ b/apex/product_packages.mk
@@ -21,7 +21,6 @@
PRODUCT_PACKAGES += \
com.android.compos \
- com.android.virt \
# TODO(b/207336449): Figure out how to get these off /system
PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST := \
@@ -33,3 +32,5 @@
PRODUCT_SYSTEM_EXT_PROPERTIES := ro.config.isolated_compilation_enabled=true
PRODUCT_FSVERITY_GENERATE_METADATA := true
+
+PRODUCT_AVF_ENABLED := true
diff --git a/apkdmverity/src/main.rs b/apkdmverity/src/main.rs
index a69b583..6e12e38 100644
--- a/apkdmverity/src/main.rs
+++ b/apkdmverity/src/main.rs
@@ -162,7 +162,7 @@
}
fn create_block_aligned_file(path: &Path, data: &[u8]) {
- let mut f = File::create(&path).unwrap();
+ let mut f = File::create(path).unwrap();
f.write_all(data).unwrap();
// Add padding so that the size of the file is multiple of 4096.
diff --git a/authfs/aidl/com/android/virt/fs/IAuthFsService.aidl b/authfs/aidl/com/android/virt/fs/IAuthFsService.aidl
index b349db2..30cc281 100644
--- a/authfs/aidl/com/android/virt/fs/IAuthFsService.aidl
+++ b/authfs/aidl/com/android/virt/fs/IAuthFsService.aidl
@@ -21,6 +21,8 @@
/** @hide */
interface IAuthFsService {
+ const String AUTHFS_SERVICE_SOCKET_NAME = "authfs_service";
+
/**
* Creates an AuthFS mount given the config. Returns the binder object that represent the AuthFS
* instance. The AuthFS setup is deleted once the lifetime of the returned binder object ends.
diff --git a/authfs/service/Android.bp b/authfs/service/Android.bp
index e9eec1e..de6326d 100644
--- a/authfs/service/Android.bp
+++ b/authfs/service/Android.bp
@@ -16,6 +16,7 @@
"liblibc",
"liblog_rust",
"libnix",
+ "librpcbinder_rs",
"libshared_child",
],
prefer_rlib: true,
diff --git a/authfs/service/authfs_service.rc b/authfs/service/authfs_service.rc
index 9ad0ce6..7edb1ca 100644
--- a/authfs/service/authfs_service.rc
+++ b/authfs/service/authfs_service.rc
@@ -1,2 +1,3 @@
service authfs_service /system/bin/authfs_service
disabled
+ socket authfs_service stream 0666 root system
diff --git a/authfs/service/src/main.rs b/authfs/service/src/main.rs
index 77cac9a..671c06a 100644
--- a/authfs/service/src/main.rs
+++ b/authfs/service/src/main.rs
@@ -22,8 +22,9 @@
mod authfs;
-use anyhow::{bail, Context, Result};
+use anyhow::{bail, Result};
use log::*;
+use rpcbinder::run_init_unix_domain_rpc_server;
use std::ffi::OsString;
use std::fs::{create_dir, read_dir, remove_dir_all, remove_file};
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -31,13 +32,10 @@
use authfs_aidl_interface::aidl::com::android::virt::fs::AuthFsConfig::AuthFsConfig;
use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFs::IAuthFs;
use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFsService::{
- BnAuthFsService, IAuthFsService,
+ BnAuthFsService, IAuthFsService, AUTHFS_SERVICE_SOCKET_NAME,
};
-use binder::{
- self, add_service, BinderFeatures, ExceptionCode, Interface, ProcessState, Status, Strong,
-};
+use binder::{self, BinderFeatures, ExceptionCode, Interface, Status, Strong};
-const SERVICE_NAME: &str = "authfs_service";
const SERVICE_ROOT: &str = "/data/misc/authfs";
/// Implementation of `IAuthFsService`.
@@ -117,15 +115,17 @@
clean_up_working_directory()?;
- ProcessState::start_thread_pool();
-
let service = AuthFsService::new_binder(debuggable).as_binder();
- add_service(SERVICE_NAME, service)
- .with_context(|| format!("Failed to register service {}", SERVICE_NAME))?;
- debug!("{} is running", SERVICE_NAME);
-
- ProcessState::join_thread_pool();
- bail!("Unexpected exit after join_thread_pool")
+ debug!("{} is starting as a rpc service.", AUTHFS_SERVICE_SOCKET_NAME);
+ let retval = run_init_unix_domain_rpc_server(service, AUTHFS_SERVICE_SOCKET_NAME, || {
+ info!("The RPC server '{}' is running.", AUTHFS_SERVICE_SOCKET_NAME);
+ });
+ if retval {
+ info!("The RPC server at '{}' has shut down gracefully.", AUTHFS_SERVICE_SOCKET_NAME);
+ Ok(())
+ } else {
+ bail!("Premature termination of the RPC server '{}'.", AUTHFS_SERVICE_SOCKET_NAME)
+ }
}
fn main() {
diff --git a/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java b/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
index 8cee496..e67a309 100644
--- a/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
+++ b/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
@@ -28,12 +28,13 @@
import android.platform.test.annotations.RootPermissionTest;
import com.android.fs.common.AuthFsTestRule;
+import com.android.microdroid.test.common.DeviceProperties;
import com.android.microdroid.test.common.MetricsProcessor;
-import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.metrics.proto.MetricMeasurement.DataType;
import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import org.junit.After;
import org.junit.AfterClass;
@@ -53,7 +54,7 @@
@RootPermissionTest
@RunWith(DeviceJUnit4Parameterized.class)
@UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class)
-public class AuthFsBenchmarks extends MicrodroidHostTestCaseBase {
+public class AuthFsBenchmarks extends BaseHostJUnit4Test {
private static final int TRIAL_COUNT = 5;
/** Name of the measure_io binary on host. */
@@ -83,10 +84,10 @@
AuthFsTestRule.setUpAndroid(getTestInformation());
mAuthFsTestRule.setUpTest();
assumeTrue(AuthFsTestRule.getDevice().supportsMicrodroid(mProtectedVm));
- assumeFalse("Skip on CF; protected VM not supported", isCuttlefish());
- String metricsPrefix =
- MetricsProcessor.getMetricPrefix(
- getDevice().getProperty("debug.hypervisor.metrics_tag"));
+ DeviceProperties deviceProperties = DeviceProperties.create(getDevice()::getProperty);
+ assumeFalse(
+ "Skip on CF; no need to collect metrics on CF", deviceProperties.isCuttlefish());
+ String metricsPrefix = MetricsProcessor.getMetricPrefix(deviceProperties.getMetricsTag());
mMetricsProcessor = new MetricsProcessor(metricsPrefix + "authfs/");
AuthFsTestRule.startMicrodroid(mProtectedVm);
}
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 68e1948..601c6fc 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -32,10 +32,8 @@
use log::{info, warn};
use rustutils::system_properties;
use std::fs::{self, File};
-use std::io::{BufRead, BufReader};
use std::num::NonZeroU32;
use std::path::{Path, PathBuf};
-use std::thread;
use vmclient::{DeathReason, ErrorCode, VmInstance, VmWaitError};
/// This owns an instance of the CompOS VM.
@@ -240,12 +238,7 @@
struct Callback {}
impl vmclient::VmCallback for Callback {
- fn on_payload_started(&self, cid: i32, stream: Option<&File>) {
- if let Some(file) = stream {
- if let Err(e) = start_logging(file) {
- warn!("Can't log vm output: {}", e);
- };
- }
+ fn on_payload_started(&self, cid: i32) {
log::info!("VM payload started, cid = {}", cid);
}
@@ -265,19 +258,3 @@
log::warn!("VM died, cid = {}, reason = {:?}", cid, death_reason);
}
}
-
-fn start_logging(file: &File) -> Result<()> {
- let reader = BufReader::new(file.try_clone().context("Cloning file failed")?);
- thread::spawn(move || {
- for line in reader.lines() {
- match line {
- Ok(line) => info!("VM: {}", line),
- Err(e) => {
- warn!("Reading VM output failed: {}", e);
- break;
- }
- }
- }
- });
- Ok(())
-}
diff --git a/compos/src/artifact_signer.rs b/compos/src/artifact_signer.rs
index e51b8dd..d3843fc 100644
--- a/compos/src/artifact_signer.rs
+++ b/compos/src/artifact_signer.rs
@@ -46,7 +46,7 @@
pub fn add_artifact(&mut self, path: &Path) -> Result<()> {
// The path we store is where the file will be when it is verified, not where it is now.
let suffix = path
- .strip_prefix(&self.base_directory)
+ .strip_prefix(self.base_directory)
.context("Artifacts must be under base directory")?;
let target_path = Path::new(TARGET_DIRECTORY).join(suffix);
let target_path = target_path.to_str().ok_or_else(|| anyhow!("Invalid path"))?;
diff --git a/compos/src/compsvc.rs b/compos/src/compsvc.rs
index 4330bbf..40d14d8 100644
--- a/compos/src/compsvc.rs
+++ b/compos/src/compsvc.rs
@@ -30,14 +30,16 @@
use crate::artifact_signer::ArtifactSigner;
use crate::compilation::odrefresh;
use crate::compos_key;
+use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFsService::{
+ IAuthFsService, AUTHFS_SERVICE_SOCKET_NAME,
+};
use binder::{BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, Strong};
use compos_aidl_interface::aidl::com::android::compos::ICompOsService::{
BnCompOsService, ICompOsService, OdrefreshArgs::OdrefreshArgs,
};
use compos_common::binder::to_binder_result;
use compos_common::odrefresh::{is_system_property_interesting, ODREFRESH_PATH};
-
-const AUTHFS_SERVICE_NAME: &str = "authfs_service";
+use rpcbinder::get_unix_domain_rpc_interface;
/// Constructs a binder object that implements ICompOsService.
pub fn new_binder() -> Result<Strong<dyn ICompOsService>> {
@@ -127,8 +129,10 @@
impl CompOsService {
fn do_odrefresh(&self, args: &OdrefreshArgs) -> Result<i8> {
- let authfs_service = binder::get_interface(AUTHFS_SERVICE_NAME)
- .context("Unable to connect to AuthFS service")?;
+ log::debug!("Prepare to connect to {}", AUTHFS_SERVICE_SOCKET_NAME);
+ let authfs_service: Strong<dyn IAuthFsService> =
+ get_unix_domain_rpc_interface(AUTHFS_SERVICE_SOCKET_NAME)
+ .with_context(|| format!("Failed to connect to {}", AUTHFS_SERVICE_SOCKET_NAME))?;
let exit_code = odrefresh(&self.odrefresh_path, args, authfs_service, |output_dir| {
// authfs only shows us the files we created, so it's ok to just sign everything
// under the output directory.
@@ -144,7 +148,7 @@
fn add_artifacts(target_dir: &Path, artifact_signer: &mut ArtifactSigner) -> Result<()> {
for entry in
- read_dir(&target_dir).with_context(|| format!("Traversing {}", target_dir.display()))?
+ read_dir(target_dir).with_context(|| format!("Traversing {}", target_dir.display()))?
{
let entry = entry?;
let file_type = entry.file_type()?;
diff --git a/demo/Android.bp b/demo/Android.bp
index 8613166..2b234a6 100644
--- a/demo/Android.bp
+++ b/demo/Android.bp
@@ -13,7 +13,10 @@
"com.google.android.material_material",
],
libs: [
- "android.system.virtualmachine",
+ // We need to compile against the .impl library which includes the hidden
+ // APIs. Once the APIs are promoted to @SystemApi we can switch to
+ // framework-virtualization, which contains API stubs.
+ "framework-virtualization.impl",
],
jni_libs: ["MicrodroidTestNativeLib"],
platform_apis: true,
diff --git a/demo/AndroidManifest.xml b/demo/AndroidManifest.xml
index 6669adb..17a7680 100644
--- a/demo/AndroidManifest.xml
+++ b/demo/AndroidManifest.xml
@@ -4,11 +4,11 @@
<uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
<uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33"/>
+ <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
<application
android:label="MicrodroidDemo"
android:theme="@style/Theme.MicrodroidDemo"
android:testOnly="true">
- <uses-library android:name="android.system.virtualmachine" android:required="true" />
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index b5ae3d5..8e870ea 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -42,7 +42,6 @@
import com.android.microdroid.testservice.ITestService;
import java.io.BufferedReader;
-import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -169,16 +168,7 @@
private final ExecutorService mService = mExecutorService;
@Override
- public void onPayloadStarted(VirtualMachine vm,
- ParcelFileDescriptor stream) {
- if (stream == null) {
- mPayloadOutput.postValue("(no output available)");
- return;
- }
-
- InputStream input = new FileInputStream(stream.getFileDescriptor());
- mService.execute(new Reader("payload", mPayloadOutput, input));
- }
+ public void onPayloadStarted(VirtualMachine vm) {}
@Override
public void onPayloadReady(VirtualMachine vm) {
diff --git a/javalib/Android.bp b/javalib/Android.bp
index cb03fa1..2982a32 100644
--- a/javalib/Android.bp
+++ b/javalib/Android.bp
@@ -11,9 +11,10 @@
}
java_sdk_library {
- name: "android.system.virtualmachine",
- installable: true,
- compile_dex: true,
+ name: "framework-virtualization",
+
+ // TODO(b/243512044): introduce non-updatable-framework-module-defaults
+ defaults: ["framework-module-defaults"],
jarjar_rules: "jarjar-rules.txt",
@@ -25,6 +26,7 @@
],
apex_available: ["com.android.virt"],
+
permitted_packages: [
"android.system.virtualmachine",
"android.system.virtualizationservice",
@@ -32,12 +34,34 @@
"com.android.system.virtualmachine.sysprop",
],
errorprone: {
- // We use @GuardedBy and we want a test failure if our locking isn't consistent with it.
enabled: true,
javacflags: [
+ // We use @GuardedBy and we want a test failure if our locking isn't consistent with it.
"-Xep:GuardedBy:ERROR",
+ // JavaApiUsedByMainlineModule is quite spammy, and since we com.android.virt is not
+ // an updatable module we don't need it.
+ "-Xep:JavaApiUsedByMainlineModule:OFF",
],
},
+
+ test: {
+ enabled: true,
+ sdk_version: "module_current",
+ },
+
+ sdk_version: "core_platform",
+ stub_only_libs: [
+ "android_module_lib_stubs_current",
+ ],
+ impl_only_libs: [
+ "framework",
+ ],
+ impl_library_visibility: [
+ "//packages/modules/Virtualization:__subpackages__",
+ ],
+
+ // TODO(b/243512044): remove once we have API tracking files in prebuilts/sdk
+ unsafe_ignore_missing_latest_api: true,
}
prebuilt_apis {
diff --git a/javalib/api/module-lib-current.txt b/javalib/api/module-lib-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/javalib/api/module-lib-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/javalib/api/module-lib-removed.txt b/javalib/api/module-lib-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/javalib/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index cf791e4..4435576 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -16,6 +16,7 @@
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;
@@ -75,7 +76,7 @@
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;
@@ -84,10 +85,7 @@
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;
@@ -105,11 +103,6 @@
* @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";
@@ -206,17 +199,10 @@
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.
+ // You can take mLock while holding VirtualMachineManager.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();
@@ -279,16 +265,44 @@
mExtraApks = setupExtraApks(context, config, thisVmDir);
}
- @GuardedBy("sCreateLock")
+ /**
+ * 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("VirtualMachineManager.sCreateLock")
@NonNull
- private static Map<String, WeakReference<VirtualMachine>> getInstancesMap(Context context) {
- return sInstances.computeIfAbsent(context, unused -> new HashMap<>());
- }
-
- @NonNull
- private static File getVmDir(Context context, String name) {
- File vmRoot = new File(context.getDataDir(), VM_DIR);
- return new File(vmRoot, name);
+ 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());
+ 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;
+ }
}
/**
@@ -296,36 +310,16 @@
* 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")
+ @GuardedBy("VirtualMachineManager.sCreateLock")
@NonNull
static VirtualMachine create(
@NonNull Context context, @NonNull String name, @NonNull VirtualMachineConfig config)
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);
- }
+ File vmDir = createVmDir(context, name);
try {
VirtualMachine vm = new VirtualMachine(context, name, config);
-
- try (FileOutputStream output = new FileOutputStream(vm.mConfigFilePath)) {
- config.serialize(output);
- } catch (IOException e) {
- throw new VirtualMachineException("failed to write VM config", e);
- }
-
+ config.serialize(vm.mConfigFilePath);
try {
vm.mInstanceFilePath.createNewFile();
} catch (IOException e) {
@@ -348,9 +342,6 @@
} 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
@@ -364,65 +355,68 @@
}
/** Loads a virtual machine that is already created before. */
- @GuardedBy("sCreateLock")
+ @GuardedBy("VirtualMachineManager.sCreateLock")
@Nullable
- static VirtualMachine load(
- @NonNull Context context, @NonNull String name) throws VirtualMachineException {
+ 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;
- try (FileInputStream input = new FileInputStream(configFilePath)) {
- config = VirtualMachineConfig.from(input);
- } catch (IOException e) {
- throw new VirtualMachineException("Failed to read config file", e);
- }
-
- 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);
- }
+ VirtualMachineConfig config = VirtualMachineConfig.from(configFilePath);
+ VirtualMachine 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;
+ @GuardedBy("VirtualMachineManager.sCreateLock")
+ void delete(Context context, String name) throws VirtualMachineException {
+ synchronized (mLock) {
+ checkStopped();
}
- if (vm != null) {
- synchronized (vm.mLock) {
- vm.checkStopped();
- }
- }
+ deleteVmDirectory(context, name);
+ }
+ static void deleteVmDirectory(Context context, String name) throws VirtualMachineException {
try {
deleteRecursively(getVmDir(context, name));
} catch (IOException e) {
throw new VirtualMachineException(e);
}
+ }
- if (instancesMap != null) instancesMap.remove(name);
+ @GuardedBy("VirtualMachineManager.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);
}
/**
@@ -643,9 +637,8 @@
mVirtualMachine.registerCallback(
new IVirtualMachineCallback.Stub() {
@Override
- public void onPayloadStarted(int cid, ParcelFileDescriptor stream) {
- executeCallback(
- (cb) -> cb.onPayloadStarted(VirtualMachine.this, stream));
+ public void onPayloadStarted(int cid) {
+ executeCallback((cb) -> cb.onPayloadStarted(VirtualMachine.this));
}
@Override
@@ -656,16 +649,20 @@
@Override
public void onPayloadFinished(int cid, int exitCode) {
executeCallback(
- (cb) -> cb.onPayloadFinished(VirtualMachine.this,
- exitCode));
+ (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));
+ (cb) ->
+ cb.onError(
+ VirtualMachine.this,
+ translatedError,
+ message));
}
@Override
@@ -674,18 +671,17 @@
int translatedReason = getTranslatedReason(reason);
if (onDiedCalled.compareAndSet(false, true)) {
executeCallback(
- (cb) -> cb.onStopped(VirtualMachine.this,
- translatedReason));
+ (cb) ->
+ cb.onStopped(
+ VirtualMachine.this, translatedReason));
}
}
@Override
public void onRamdump(int cid, ParcelFileDescriptor ramdump) {
- executeCallback(
- (cb) -> cb.onRamdump(VirtualMachine.this, ramdump));
+ executeCallback((cb) -> cb.onRamdump(VirtualMachine.this, ramdump));
}
- }
- );
+ });
service.asBinder().linkToDeath(deathRecipient, 0);
mVirtualMachine.start();
} catch (IOException | IllegalStateException | ServiceSpecificException e) {
@@ -838,14 +834,7 @@
throw new VirtualMachineException("incompatible config");
}
checkStopped();
-
- try {
- FileOutputStream output = new FileOutputStream(mConfigFilePath);
- newConfig.serialize(output);
- output.close();
- } catch (IOException e) {
- throw new VirtualMachineException("Failed to persist config", e);
- }
+ newConfig.serialize(mConfigFilePath);
mConfig = newConfig;
return oldConfig;
}
@@ -901,6 +890,7 @@
* @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 {
@@ -1065,4 +1055,14 @@
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);
+ }
+ }
}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
index bb6b2b8..1f94a8b 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -18,7 +18,6 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.os.ParcelFileDescriptor;
@@ -135,11 +134,8 @@
/** The VM killed due to hangup */
int STOP_REASON_HANGUP = 16;
- /**
- * Called when the payload starts in the VM. The stream, if non-null, provides access
- * to the stdin/stdout of the VM payload.
- */
- void onPayloadStarted(@NonNull VirtualMachine vm, @Nullable ParcelFileDescriptor stream);
+ /** Called when the payload starts in the VM. */
+ void onPayloadStarted(@NonNull VirtualMachine vm);
/**
* Called when the payload in the VM is ready to serve. See
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index b814367..a660306 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -16,6 +16,7 @@
package android.system.virtualmachine;
+import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
import static java.util.Objects.requireNonNull;
@@ -33,7 +34,9 @@
import android.system.virtualizationservice.VirtualMachinePayloadConfig;
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;
@@ -181,9 +184,30 @@
mNumCpus = numCpus;
}
+ /** 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
- static VirtualMachineConfig from(@NonNull InputStream input)
+ private static VirtualMachineConfig fromInputStream(@NonNull InputStream input)
throws IOException, VirtualMachineException {
PersistableBundle b = PersistableBundle.readFromStream(input);
int version = b.getInt(KEY_VERSION);
@@ -215,8 +239,17 @@
protectedVm, memoryMib, numCpus);
}
+ /** 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. */
- void serialize(@NonNull OutputStream output) throws IOException {
+ private void serializeOutputStream(@NonNull OutputStream output) throws IOException {
PersistableBundle b = new PersistableBundle();
b.putInt(KEY_VERSION, VERSION);
b.putString(KEY_APKPATH, mApkPath);
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
index 70532fc..b51cbce 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -23,8 +23,6 @@
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
-import com.android.internal.annotations.VisibleForTesting;
-
/**
* A VM descriptor that captures the state of a Virtual Machine.
*
@@ -35,8 +33,8 @@
* @hide
*/
public final class VirtualMachineDescriptor implements Parcelable {
- private final @NonNull ParcelFileDescriptor mConfigFd;
- private final @NonNull ParcelFileDescriptor mInstanceImgFd;
+ @NonNull private final ParcelFileDescriptor mConfigFd;
+ @NonNull private final ParcelFileDescriptor mInstanceImgFd;
// TODO(b/243129654): Add trusted storage fd once it is available.
@Override
@@ -45,13 +43,14 @@
}
@Override
- public void writeToParcel(Parcel out, int flags) {
+ public void writeToParcel(@NonNull Parcel out, int flags) {
mConfigFd.writeToParcel(out, flags);
mInstanceImgFd.writeToParcel(out, flags);
}
+ @NonNull
public static final Parcelable.Creator<VirtualMachineDescriptor> CREATOR =
- new Parcelable.Creator<VirtualMachineDescriptor>() {
+ new Parcelable.Creator<>() {
public VirtualMachineDescriptor createFromParcel(Parcel in) {
return new VirtualMachineDescriptor(in);
}
@@ -63,19 +62,17 @@
/**
* @return File descriptor of the VM configuration file config.xml.
- * @hide
*/
- @VisibleForTesting
- public @NonNull ParcelFileDescriptor getConfigFd() {
+ @NonNull
+ ParcelFileDescriptor getConfigFd() {
return mConfigFd;
}
/**
* @return File descriptor of the instance.img of the VM.
- * @hide
*/
- @VisibleForTesting
- public @NonNull ParcelFileDescriptor getInstanceImgFd() {
+ @NonNull
+ ParcelFileDescriptor getInstanceImgFd() {
return mInstanceImgFd;
}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index 34b9fd9..0e96f43 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -25,6 +25,7 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.sysprop.HypervisorProperties;
+import android.util.ArrayMap;
import com.android.internal.annotations.GuardedBy;
@@ -47,6 +48,13 @@
* @hide
*/
public class VirtualMachineManager {
+ /**
+ * A lock used to synchronize the creation of virtual machines. It protects {@link #mVmsByName},
+ * but is also held throughout VM creation / retrieval / deletion, to prevent these actions
+ * racing with each other.
+ */
+ private static final Object sCreateLock = new Object();
+
@NonNull private final Context mContext;
private VirtualMachineManager(@NonNull Context context) {
@@ -57,6 +65,9 @@
private static final Map<Context, WeakReference<VirtualMachineManager>> sInstances =
new WeakHashMap<>();
+ @GuardedBy("sCreateLock")
+ private final Map<String, WeakReference<VirtualMachine>> mVmsByName = new ArrayMap<>();
+
/**
* Capabilities of the virtual machine implementation.
*
@@ -136,23 +147,65 @@
public VirtualMachine create(
@NonNull String name, @NonNull VirtualMachineConfig config)
throws VirtualMachineException {
- synchronized (VirtualMachine.sCreateLock) {
- return VirtualMachine.create(mContext, name, config);
+ synchronized (sCreateLock) {
+ return createLocked(name, config);
}
}
+ @NonNull
+ @GuardedBy("sCreateLock")
+ private VirtualMachine createLocked(@NonNull String name, @NonNull VirtualMachineConfig config)
+ throws VirtualMachineException {
+ VirtualMachine vm = VirtualMachine.create(mContext, name, config);
+ mVmsByName.put(name, new WeakReference<>(vm));
+ return vm;
+ }
+
/**
* Returns an existing {@link VirtualMachine} with the given name. Returns null if there is no
* such virtual machine.
*
* @throws VirtualMachineException if the virtual machine exists but could not be successfully
- * retrieved.
+ * retrieved.
* @hide
*/
@Nullable
public VirtualMachine get(@NonNull String name) throws VirtualMachineException {
- synchronized (VirtualMachine.sCreateLock) {
- return VirtualMachine.load(mContext, name);
+ synchronized (sCreateLock) {
+ return getLocked(name);
+ }
+ }
+
+ @Nullable
+ @GuardedBy("sCreateLock")
+ private VirtualMachine getLocked(@NonNull String name) throws VirtualMachineException {
+ VirtualMachine vm = getVmByName(name);
+ if (vm != null) return vm;
+
+ vm = VirtualMachine.load(mContext, name);
+ if (vm != null) {
+ mVmsByName.put(name, new WeakReference<>(vm));
+ }
+ return vm;
+ }
+
+ /**
+ * Imports 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.
+ *
+ * @throws VirtualMachineException if the VM cannot be imported.
+ * @hide
+ */
+ @NonNull
+ public VirtualMachine importFromDescriptor(
+ @NonNull String name, @NonNull VirtualMachineDescriptor vmDescriptor)
+ throws VirtualMachineException {
+ synchronized (sCreateLock) {
+ VirtualMachine vm = VirtualMachine.fromDescriptor(mContext, name, vmDescriptor);
+ mVmsByName.put(name, new WeakReference<>(vm));
+ return vm;
}
}
@@ -167,14 +220,14 @@
public VirtualMachine getOrCreate(
@NonNull String name, @NonNull VirtualMachineConfig config)
throws VirtualMachineException {
- VirtualMachine vm;
- synchronized (VirtualMachine.sCreateLock) {
- vm = get(name);
- if (vm == null) {
- vm = create(name, config);
+ synchronized (sCreateLock) {
+ VirtualMachine vm = getLocked(name);
+ if (vm != null) {
+ return vm;
+ } else {
+ return createLocked(name, config);
}
}
- return vm;
}
/**
@@ -189,9 +242,28 @@
* @hide
*/
public void delete(@NonNull String name) throws VirtualMachineException {
- requireNonNull(name);
- synchronized (VirtualMachine.sCreateLock) {
- VirtualMachine.delete(mContext, name);
+ synchronized (sCreateLock) {
+ VirtualMachine vm = getVmByName(name);
+ if (vm == null) {
+ VirtualMachine.deleteVmDirectory(mContext, name);
+ } else {
+ vm.delete(mContext, name);
+ }
+ mVmsByName.remove(name);
}
}
+
+ @Nullable
+ @GuardedBy("sCreateLock")
+ private VirtualMachine getVmByName(@NonNull String name) {
+ requireNonNull(name);
+ WeakReference<VirtualMachine> weakReference = mVmsByName.get(name);
+ if (weakReference != null) {
+ VirtualMachine vm = weakReference.get();
+ if (vm != null && vm.getStatus() != VirtualMachine.STATUS_DELETED) {
+ return vm;
+ }
+ }
+ return null;
+ }
}
diff --git a/libs/devicemapper/src/lib.rs b/libs/devicemapper/src/lib.rs
index b9fb5c3..ebe71e4 100644
--- a/libs/devicemapper/src/lib.rs
+++ b/libs/devicemapper/src/lib.rs
@@ -212,7 +212,7 @@
dm_dev_suspend(self, &mut data).context("failed to activate")?;
// Step 4: wait unti the device is created and return the device path
- let path = Path::new(MAPPER_DEV_ROOT).join(&name);
+ let path = Path::new(MAPPER_DEV_ROOT).join(name);
wait_for_path(&path)?;
Ok(path)
}
@@ -250,13 +250,13 @@
}
fn write_to_dev(path: &Path, data: &[u8]) {
- let mut f = OpenOptions::new().read(true).write(true).open(&path).unwrap();
+ let mut f = OpenOptions::new().read(true).write(true).open(path).unwrap();
f.write_all(data).unwrap();
}
fn delete_device(dm: &DeviceMapper, name: &str) -> Result<()> {
dm.delete_device_deferred(name)?;
- wait_for_path_disappears(Path::new(MAPPER_DEV_ROOT).join(&name))?;
+ wait_for_path_disappears(Path::new(MAPPER_DEV_ROOT).join(name))?;
Ok(())
}
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 68827df..d73a03c 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -72,7 +72,6 @@
"debuggerd",
"linker",
"linkerconfig",
- "servicemanager.microdroid",
"tombstoned.microdroid",
"tombstone_transmit.microdroid",
"cgroups.json",
diff --git a/microdroid/init.rc b/microdroid/init.rc
index 9c62782..94ef940 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -26,20 +26,6 @@
setprop ro.log.file_logger.path /dev/null
on init
- # Mount binderfs
- mkdir /dev/binderfs
- mount binder binder /dev/binderfs stats=global
- chmod 0755 /dev/binderfs
-
- symlink /dev/binderfs/binder /dev/binder
- symlink /dev/binderfs/vndbinder /dev/vndbinder
-
- chmod 0666 /dev/binderfs/binder
- chmod 0666 /dev/binderfs/vndbinder
-
- start servicemanager
-
-on init
mkdir /mnt/apk 0755 system system
mkdir /mnt/extra-apk 0755 root root
# Microdroid_manager starts apkdmverity/zipfuse/apexd
diff --git a/microdroid/vm_payload/Android.bp b/microdroid/vm_payload/Android.bp
index e153f92..dd2a937 100644
--- a/microdroid/vm_payload/Android.bp
+++ b/microdroid/vm_payload/Android.bp
@@ -14,6 +14,7 @@
"libanyhow",
"libbinder_rs",
"liblazy_static",
+ "liblibc",
"liblog_rust",
"librpcbinder_rs",
],
diff --git a/microdroid/vm_payload/include/vm_payload.h b/microdroid/vm_payload/include/vm_payload.h
index 82dbd6d..48518ff 100644
--- a/microdroid/vm_payload/include/vm_payload.h
+++ b/microdroid/vm_payload/include/vm_payload.h
@@ -56,8 +56,9 @@
void (*on_ready)(void *param), void *param);
/**
- * Get a secret that is uniquely bound to this VM instance. The secrets are 32-byte values and the
- * value associated with an identifier will not change over the lifetime of the VM instance.
+ * Get a secret that is uniquely bound to this VM instance. The secrets are
+ * values up to 32 bytes long and the value associated with an identifier will
+ * not change over the lifetime of the VM instance.
*
* \param identifier identifier of the secret to return.
* \param identifier_size size of the secret identifier.
@@ -80,4 +81,17 @@
*/
const char *AVmPayload_getApkContentsPath(void);
+/**
+ * Gets the path to the encrypted persistent storage for the VM, if any. This is
+ * a directory under which any files or directories created will be stored on
+ * behalf of the VM by the host app. All data is encrypted using a key known
+ * only to the VM, so the host cannot decrypt it, but may delete it.
+ *
+ * \return the path to the APK contents, or NULL if no encrypted storage was
+ * requested in the VM configuration. If non-null the returned string should not
+ * be deleted or freed by the application and remains valid for the lifetime of
+ * the VM.
+ */
+const char *AVmPayload_getEncryptedStoragePath(void);
+
__END_DECLS
diff --git a/microdroid/vm_payload/src/vm_payload_service.rs b/microdroid/vm_payload/src/vm_payload_service.rs
index 098d246..88484cc 100644
--- a/microdroid/vm_payload/src/vm_payload_service.rs
+++ b/microdroid/vm_payload/src/vm_payload_service.rs
@@ -23,19 +23,44 @@
use rpcbinder::{get_unix_domain_rpc_interface, run_vsock_rpc_server};
use std::ffi::CString;
use std::os::raw::{c_char, c_void};
+use std::ptr;
+use std::sync::Mutex;
lazy_static! {
static ref VM_APK_CONTENTS_PATH_C: CString =
CString::new(VM_APK_CONTENTS_PATH).expect("CString::new failed");
+ static ref PAYLOAD_CONNECTION: Mutex<Option<Strong<dyn IVmPayloadService>>> = Mutex::default();
+}
+
+/// Return a connection to the payload service in Microdroid Manager. Uses the existing connection
+/// if there is one, otherwise attempts to create a new one.
+fn get_vm_payload_service() -> Result<Strong<dyn IVmPayloadService>> {
+ let mut connection = PAYLOAD_CONNECTION.lock().unwrap();
+ if let Some(strong) = &*connection {
+ Ok(strong.clone())
+ } else {
+ let new_connection: Strong<dyn IVmPayloadService> = get_unix_domain_rpc_interface(
+ VM_PAYLOAD_SERVICE_SOCKET_NAME,
+ )
+ .context(format!("Failed to connect to service: {}", VM_PAYLOAD_SERVICE_SOCKET_NAME))?;
+ *connection = Some(new_connection.clone());
+ Ok(new_connection)
+ }
+}
+
+/// Make sure our logging goes to logcat. It is harmless to call this more than once.
+fn initialize_logging() {
+ android_logger::init_once(
+ android_logger::Config::default().with_tag("vm_payload").with_min_level(Level::Debug),
+ );
}
/// Notifies the host that the payload is ready.
/// Returns true if the notification succeeds else false.
#[no_mangle]
pub extern "C" fn AVmPayload_notifyPayloadReady() -> bool {
- android_logger::init_once(
- android_logger::Config::default().with_tag("vm_payload").with_min_level(Level::Debug),
- );
+ initialize_logging();
+
if let Err(e) = try_notify_payload_ready() {
error!("{:?}", e);
false
@@ -74,6 +99,8 @@
on_ready: Option<unsafe extern "C" fn(param: *mut c_void)>,
param: *mut c_void,
) -> bool {
+ initialize_logging();
+
// SAFETY: AIBinder returned has correct reference count, and the ownership can
// safely be taken by new_spibinder.
let service = new_spibinder(service);
@@ -106,6 +133,8 @@
secret: *mut u8,
size: usize,
) -> bool {
+ initialize_logging();
+
let identifier = std::slice::from_raw_parts(identifier, identifier_size);
match try_get_vm_instance_secret(identifier, size) {
Err(e) => {
@@ -145,6 +174,8 @@
size: usize,
total: *mut usize,
) -> bool {
+ initialize_logging();
+
match try_get_dice_attestation_chain() {
Err(e) => {
error!("{:?}", e);
@@ -179,6 +210,8 @@
size: usize,
total: *mut usize,
) -> bool {
+ initialize_logging();
+
match try_get_dice_attestation_cdi() {
Err(e) => {
error!("{:?}", e);
@@ -198,11 +231,13 @@
(*VM_APK_CONTENTS_PATH_C).as_ptr()
}
-fn try_get_dice_attestation_cdi() -> Result<Vec<u8>> {
- get_vm_payload_service()?.getDiceAttestationCdi().context("Cannot get attestation CDI")
+/// Gets the path to the VM's encrypted storage.
+#[no_mangle]
+pub extern "C" fn AVmPayload_getEncryptedStoragePath() -> *const c_char {
+ // TODO(b/254454578): Return a real path if storage is present
+ ptr::null()
}
-fn get_vm_payload_service() -> Result<Strong<dyn IVmPayloadService>> {
- get_unix_domain_rpc_interface(VM_PAYLOAD_SERVICE_SOCKET_NAME)
- .context(format!("Failed to connect to service: {}", VM_PAYLOAD_SERVICE_SOCKET_NAME))
+fn try_get_dice_attestation_cdi() -> Result<Vec<u8>> {
+ get_vm_payload_service()?.getDiceAttestationCdi().context("Cannot get attestation CDI")
}
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index cc4b9dc..4ebcc2e 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -26,9 +26,12 @@
use crate::vm_payload_service::register_vm_payload_service;
use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::ErrorCode::ErrorCode;
use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
- IVirtualMachineService, VM_BINDER_SERVICE_PORT, VM_STREAM_SERVICE_PORT,
+ IVirtualMachineService, VM_BINDER_SERVICE_PORT,
};
-use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::VM_APK_CONTENTS_PATH;
+use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
+ VM_APK_CONTENTS_PATH,
+ VM_PAYLOAD_SERVICE_SOCKET_NAME,
+};
use anyhow::{anyhow, bail, ensure, Context, Error, Result};
use apkverify::{get_public_key_der, verify, V4Signature};
use binder::Strong;
@@ -36,28 +39,28 @@
use glob::glob;
use itertools::sorted;
use libc::VMADDR_CID_HOST;
-use log::{error, info};
+use log::{error, info, warn};
use microdroid_metadata::{write_metadata, Metadata, PayloadMetadata};
use microdroid_payload_config::{OsConfig, Task, TaskType, VmPayloadConfig};
+use nix::fcntl::{fcntl, F_SETFD, FdFlag};
use nix::sys::signal::Signal;
use openssl::sha::Sha512;
use payload::{get_apex_data_from_payload, load_metadata, to_metadata};
use rand::Fill;
use rpcbinder::get_vsock_rpc_interface;
+use rustutils::sockets::android_get_control_socket;
use rustutils::system_properties;
use rustutils::system_properties::PropertyWatcher;
use std::borrow::Cow::{Borrowed, Owned};
use std::convert::TryInto;
use std::env;
-use std::fs::{self, create_dir, File, OpenOptions};
+use std::fs::{self, create_dir, OpenOptions};
use std::io::Write;
-use std::os::unix::io::{FromRawFd, IntoRawFd};
use std::os::unix::process::ExitStatusExt;
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::str;
use std::time::{Duration, SystemTime};
-use vsock::VsockStream;
const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
const MAIN_APK_PATH: &str = "/dev/block/by-name/microdroid-apk";
@@ -182,10 +185,22 @@
})
}
+fn set_cloexec_on_vm_payload_service_socket() -> Result<()> {
+ let fd = android_get_control_socket(VM_PAYLOAD_SERVICE_SOCKET_NAME)?;
+
+ fcntl(fd, F_SETFD(FdFlag::FD_CLOEXEC))?;
+
+ Ok(())
+}
+
fn try_main() -> Result<()> {
let _ = kernlog::init();
info!("started.");
+ if let Err(e) = set_cloexec_on_vm_payload_service_socket() {
+ warn!("Failed to set cloexec on vm payload socket: {:?}", e);
+ }
+
load_crashkernel_if_supported().context("Failed to load crashkernel")?;
swap::init_swap().context("Failed to initialise swap")?;
@@ -444,8 +459,6 @@
fn run_apkdmverity(args: &[ApkDmverityArgument]) -> Result<Child> {
let mut cmd = Command::new(APKDMVERITY_BIN);
- cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
-
for argument in args {
cmd.arg("--apk").arg(argument.apk).arg(argument.idsig).arg(argument.name);
if let Some(root_hash) = argument.saved_root_hash {
@@ -477,15 +490,7 @@
if let Some(property_name) = ready_prop {
cmd.args(["-p", property_name]);
}
- cmd.arg("-o")
- .arg(option)
- .arg(zip_path)
- .arg(mount_dir)
- .stdin(Stdio::null())
- .stdout(Stdio::null())
- .stderr(Stdio::null())
- .spawn()
- .context("Spawn zipfuse")
+ cmd.arg("-o").arg(option).arg(zip_path).arg(mount_dir).spawn().context("Spawn zipfuse")
}
fn write_apex_payload_data(
@@ -747,11 +752,18 @@
Ok(())
}
-/// Executes the given task. Stdout of the task is piped into the vsock stream to the
-/// virtualizationservice in the host side.
+/// Executes the given task.
fn exec_task(task: &Task, service: &Strong<dyn IVirtualMachineService>) -> Result<i32> {
info!("executing main task {:?}...", task);
- let mut command = build_command(task)?;
+ let mut command = match task.type_ {
+ TaskType::Executable => Command::new(&task.command),
+ TaskType::MicrodroidLauncher => {
+ let mut command = Command::new("/system/bin/microdroid_launcher");
+ command.arg(find_library_path(&task.command)?);
+ command
+ }
+ };
+ command.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
info!("notifying payload started");
service.notifyPayloadStarted()?;
@@ -770,40 +782,6 @@
}
}
-fn build_command(task: &Task) -> Result<Command> {
- let mut command = match task.type_ {
- TaskType::Executable => Command::new(&task.command),
- TaskType::MicrodroidLauncher => {
- let mut command = Command::new("/system/bin/microdroid_launcher");
- command.arg(find_library_path(&task.command)?);
- command
- }
- };
-
- match VsockStream::connect_with_cid_port(VMADDR_CID_HOST, VM_STREAM_SERVICE_PORT as u32) {
- Ok(stream) => {
- // SAFETY: the ownership of the underlying file descriptor is transferred from stream
- // to the file object, and then into the Command object. When the command is finished,
- // the file descriptor is closed.
- let file = unsafe { File::from_raw_fd(stream.into_raw_fd()) };
- command
- .stdin(Stdio::from(file.try_clone()?))
- .stdout(Stdio::from(file.try_clone()?))
- .stderr(Stdio::from(file));
- }
- Err(e) => {
- error!("failed to connect to virtualization service: {}", e);
- // Don't fail hard here. Even if we failed to connect to the virtualizationservice,
- // we keep executing the task. This can happen if the owner of the VM doesn't register
- // callback to accept the stream. Use /dev/null as the stream so that the task can
- // make progress without waiting for someone to consume the output.
- command.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
- }
- }
-
- Ok(command)
-}
-
fn find_library_path(name: &str) -> Result<String> {
let mut watcher = PropertyWatcher::new("ro.product.cpu.abilist")?;
let value = watcher.read(|_name, value| Ok(value.trim().to_string()))?;
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 71bac72..77de696 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -8,6 +8,9 @@
defaults: ["vmbase_ffi_defaults"],
srcs: ["src/main.rs"],
edition: "2021",
+ features: [
+ "legacy",
+ ],
rustlibs: [
"libbuddy_system_allocator",
"liblog_rust_nostd",
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index dc2087d..c0ad878 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -19,11 +19,15 @@
use crate::mmio_guard;
use core::arch::asm;
use core::slice;
-use log::{debug, error, LevelFilter};
-use vmbase::{console, logger, main, power::reboot};
+use log::debug;
+use log::error;
+use log::LevelFilter;
+use vmbase::{console, layout, logger, main, power::reboot};
#[derive(Debug, Clone)]
enum RebootReason {
+ /// A malformed BCC was received.
+ InvalidBcc,
/// An unexpected internal error happened.
InternalError,
}
@@ -80,8 +84,17 @@
RebootReason::InternalError
})?;
+ // SAFETY - We only get the appended payload from here, once. It is mapped and the linker
+ // script prevents it from overlapping with other objects.
+ let bcc = as_bcc(unsafe { get_appended_data_slice() }).ok_or_else(|| {
+ error!("Invalid BCC");
+ RebootReason::InvalidBcc
+ })?;
+
// This wrapper allows main() to be blissfully ignorant of platform details.
- crate::main(fdt, payload);
+ crate::main(fdt, payload, bcc);
+
+ // TODO: Overwrite BCC before jumping to payload to avoid leaking our sealing key.
mmio_guard::unmap(console::BASE_ADDRESS).map_err(|e| {
error!("Failed to unshare the UART: {e}");
@@ -147,3 +160,22 @@
);
};
}
+
+unsafe fn get_appended_data_slice() -> &'static mut [u8] {
+ let base = helpers::align_up(layout::binary_end(), helpers::SIZE_4KB).unwrap();
+ // pvmfw is contained in a 2MiB region so the payload can't be larger than the 2MiB alignment.
+ let size = helpers::align_up(base, helpers::SIZE_2MB).unwrap() - base;
+
+ slice::from_raw_parts_mut(base as *mut u8, size)
+}
+
+fn as_bcc(data: &mut [u8]) -> Option<&mut [u8]> {
+ const BCC_SIZE: usize = helpers::SIZE_4KB;
+
+ if cfg!(feature = "legacy") {
+ // TODO(b/256148034): return None if BccHandoverParse(bcc) != kDiceResultOk.
+ Some(&mut data[..BCC_SIZE])
+ } else {
+ None
+ }
+}
diff --git a/pvmfw/src/helpers.rs b/pvmfw/src/helpers.rs
index adfc189..59cf9f3 100644
--- a/pvmfw/src/helpers.rs
+++ b/pvmfw/src/helpers.rs
@@ -17,12 +17,25 @@
pub const SIZE_4KB: usize = 4 << 10;
pub const SIZE_2MB: usize = 2 << 20;
-/// Computes the address of the page containing a given address.
-pub const fn page_of(addr: usize, page_size: usize) -> usize {
- addr & !(page_size - 1)
+/// Computes the largest multiple of the provided alignment smaller or equal to the address.
+///
+/// Note: the result is undefined if alignment isn't a power of two.
+pub const fn unchecked_align_down(addr: usize, alignment: usize) -> usize {
+ addr & !(alignment - 1)
+}
+
+/// Safe wrapper around unchecked_align_up() that validates its assumptions and doesn't wrap.
+pub const fn align_up(addr: usize, alignment: usize) -> Option<usize> {
+ if !alignment.is_power_of_two() {
+ None
+ } else if let Some(s) = addr.checked_add(alignment - 1) {
+ Some(unchecked_align_down(s, alignment))
+ } else {
+ None
+ }
}
/// Computes the address of the 4KiB page containing a given address.
pub const fn page_4kb_of(addr: usize) -> usize {
- page_of(addr, SIZE_4KB)
+ unchecked_align_down(addr, SIZE_4KB)
}
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index c0bb263..8178d0b 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -29,7 +29,7 @@
use avb::PUBLIC_KEY;
use log::{debug, info};
-fn main(fdt: &mut [u8], payload: &[u8]) {
+fn main(fdt: &mut [u8], payload: &[u8], bcc: &[u8]) {
info!("pVM firmware");
debug!(
"fdt_address={:#018x}, payload_start={:#018x}, payload_size={:#018x}",
@@ -37,6 +37,7 @@
payload.as_ptr() as usize,
payload.len(),
);
+ debug!("BCC: {:?} ({:#x} bytes)", bcc.as_ptr(), bcc.len());
debug!("AVB public key: addr={:?}, size={:#x} ({1})", PUBLIC_KEY.as_ptr(), PUBLIC_KEY.len());
info!("Starting payload...");
}
diff --git a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
index 260f804..16e4893 100644
--- a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
@@ -25,7 +25,7 @@
*
* @return The read rate in MB/s.
*/
- double measureReadRate(String filename, long fileSizeBytes, boolean isRand);
+ double measureReadRate(String filename, boolean isRand);
/** Returns an entry from /proc/meminfo. */
long getMemInfoEntry(String name);
diff --git a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index e8c435f..eda4f75 100644
--- a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -36,4 +36,7 @@
/* get the APK contents path. */
String getApkContentsPath();
+
+ /* get the encrypted storage path. */
+ String getEncryptedStoragePath();
}
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index e6f39f8..1747183 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -16,7 +16,10 @@
"com.android.microdroid.testservice-java",
"truth-prebuilt",
],
- libs: ["android.system.virtualmachine"],
+ // We need to compile against the .impl library which includes the hidden
+ // APIs. Once the APIs are promoted to @SystemApi we can switch to
+ // framework-virtualization, which contains API stubs.
+ libs: ["framework-virtualization.impl"],
jni_libs: [
"MicrodroidBenchmarkNativeLib",
"MicrodroidIdleNativeLib",
@@ -28,20 +31,11 @@
}
cc_library_shared {
- name: "MicrodroidIdleNativeLib",
- srcs: ["src/native/idlebinary.cpp"],
- header_libs: ["vm_payload_headers"],
- shared_libs: [
- "libbase",
- ],
-}
-
-cc_library_shared {
name: "MicrodroidBenchmarkNativeLib",
- srcs: ["src/native/benchmarkbinary.cpp"],
+ srcs: ["src/native/*.cpp"],
+ local_include_dirs: ["src/native/include"],
static_libs: [
"com.android.microdroid.testservice-ndk",
- "libiobenchmark",
],
shared_libs: [
"libbase",
@@ -50,12 +44,3 @@
"libvm_payload",
],
}
-
-cc_library {
- name: "libiobenchmark",
- srcs: ["src/native/io_vsock.cpp"],
- export_include_dirs: ["src/native/include"],
- shared_libs: [
- "libbase",
- ],
-}
diff --git a/tests/benchmark/AndroidManifest.xml b/tests/benchmark/AndroidManifest.xml
index c39b91c..8a7366a 100644
--- a/tests/benchmark/AndroidManifest.xml
+++ b/tests/benchmark/AndroidManifest.xml
@@ -18,7 +18,6 @@
<uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
<uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
<application>
- <uses-library android:name="android.system.virtualmachine" android:required="false" />
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.microdroid.benchmark"
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index db74358..28852e8 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -257,18 +257,10 @@
private static class VirtioBlkListener implements BenchmarkVmListener.InnerListener {
private static final String FILENAME = APEX_ETC_FS + "microdroid_super.img";
- private final long mFileSizeBytes;
private final List<Double> mReadRates;
private final boolean mIsRand;
VirtioBlkListener(List<Double> readRates, boolean isRand) {
- File file = new File(FILENAME);
- try {
- mFileSizeBytes = Files.size(file.toPath());
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- assertThat(mFileSizeBytes).isGreaterThan((long) SIZE_MB);
mReadRates = readRates;
mIsRand = isRand;
}
@@ -276,7 +268,7 @@
@Override
public void onPayloadReady(VirtualMachine vm, IBenchmarkService benchmarkService)
throws RemoteException {
- double readRate = benchmarkService.measureReadRate(FILENAME, mFileSizeBytes, mIsRand);
+ double readRate = benchmarkService.measureReadRate(FILENAME, mIsRand);
mReadRates.add(readRate);
}
}
diff --git a/tests/benchmark/src/native/benchmarkbinary.cpp b/tests/benchmark/src/native/benchmarkbinary.cpp
index 6321c25..e43025c 100644
--- a/tests/benchmark/src/native/benchmarkbinary.cpp
+++ b/tests/benchmark/src/native/benchmarkbinary.cpp
@@ -23,6 +23,8 @@
#include <fcntl.h>
#include <linux/vm_sockets.h>
#include <stdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <vm_main.h>
@@ -56,9 +58,9 @@
class IOBenchmarkService : public aidl::com::android::microdroid::testservice::BnBenchmarkService {
public:
- ndk::ScopedAStatus measureReadRate(const std::string& filename, int64_t fileSizeBytes,
- bool isRand, double* out) override {
- auto res = measure_read_rate(filename, fileSizeBytes, isRand);
+ ndk::ScopedAStatus measureReadRate(const std::string& filename, bool isRand,
+ double* out) override {
+ auto res = measure_read_rate(filename, isRand);
if (res.ok()) {
*out = res.value();
}
@@ -90,13 +92,20 @@
}
private:
- /** Measures the read rate for reading the given file. */
- Result<double> measure_read_rate(const std::string& filename, int64_t fileSizeBytes,
- bool is_rand) {
- const int64_t block_count = fileSizeBytes / kBlockSizeBytes;
+ /**
+ * Measures the read rate for reading the given file.
+ * @return The read rate in MB/s.
+ */
+ Result<double> measure_read_rate(const std::string& filename, bool is_rand) {
+ struct stat file_stats;
+ if (stat(filename.c_str(), &file_stats) == -1) {
+ return Error() << "failed to get file stats";
+ }
+ const int64_t file_size_bytes = file_stats.st_size;
+ const int64_t block_count = file_size_bytes / kBlockSizeBytes;
std::vector<uint64_t> offsets(block_count);
for (auto i = 0; i < block_count; ++i) {
- offsets.push_back(i * kBlockSizeBytes);
+ offsets[i] = i * kBlockSizeBytes;
}
if (is_rand) {
std::mt19937 rd{std::random_device{}()};
@@ -118,8 +127,8 @@
}
}
double elapsed_seconds = ((double)clock() - start) / CLOCKS_PER_SEC;
- double read_rate = (double)fileSizeBytes / kNumBytesPerMB / elapsed_seconds;
- return {read_rate};
+ double file_size_mb = (double)file_size_bytes / kNumBytesPerMB;
+ return {file_size_mb / elapsed_seconds};
}
Result<size_t> read_meminfo_entry(const std::string& stat) {
diff --git a/tests/helper/Android.bp b/tests/helper/Android.bp
index 60d4be1..7473dab 100644
--- a/tests/helper/Android.bp
+++ b/tests/helper/Android.bp
@@ -3,15 +3,12 @@
}
java_library_static {
- name: "VirtualizationTestHelper",
- srcs: ["src/java/com/android/virt/**/*.java"],
- host_supported: true,
-}
-
-java_library_static {
name: "MicrodroidTestHelper",
srcs: ["src/java/com/android/microdroid/test/common/*.java"],
host_supported: true,
+ libs: [
+ "framework-annotations-lib",
+ ],
}
java_library_static {
@@ -21,8 +18,10 @@
"androidx.test.runner",
"androidx.test.ext.junit",
"MicrodroidTestHelper",
- "VirtualizationTestHelper",
"truth-prebuilt",
],
- libs: ["android.system.virtualmachine"],
+ // We need to compile against the .impl library which includes the hidden
+ // APIs. Once the APIs are promoted to @SystemApi we can switch to
+ // framework-virtualization, which contains API stubs.
+ libs: ["framework-virtualization.impl"],
}
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
new file mode 100644
index 0000000..1fc163b
--- /dev/null
+++ b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2022 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 com.android.microdroid.test.common;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/** This class can be used in both host tests and device tests to get the device properties. */
+public final class DeviceProperties {
+ /** PropertyGetter is used to get the property associated to a given key. */
+ public interface PropertyGetter {
+ @Nullable
+ String getProperty(@NonNull String key) throws Exception;
+ }
+
+ private static final String KEY_VENDOR_DEVICE = "ro.product.vendor.device";
+ private static final String KEY_METRICS_TAG = "debug.hypervisor.metrics_tag";
+
+ private static final String CUTTLEFISH_DEVICE_PREFIX = "vsoc_";
+
+ @NonNull private final PropertyGetter mPropertyGetter;
+
+ private DeviceProperties(@NonNull PropertyGetter propertyGetter) {
+ mPropertyGetter = requireNonNull(propertyGetter);
+ }
+
+ /** Creates a new instance of {@link DeviceProperties}. */
+ @NonNull
+ public static DeviceProperties create(@NonNull PropertyGetter propertyGetter) {
+ return new DeviceProperties(propertyGetter);
+ }
+
+ /**
+ * @return whether the device is a cuttlefish device.
+ */
+ public boolean isCuttlefish() {
+ String vendorDeviceName = getProperty(KEY_VENDOR_DEVICE);
+ return vendorDeviceName != null && vendorDeviceName.startsWith(CUTTLEFISH_DEVICE_PREFIX);
+ }
+
+ @Nullable
+ public String getMetricsTag() {
+ return getProperty(KEY_METRICS_TAG);
+ }
+
+ private String getProperty(String key) {
+ try {
+ return mPropertyGetter.getProperty(key);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Cannot get property for the key: " + key, e);
+ }
+ }
+}
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 9fb7d91..24e2049 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -15,9 +15,9 @@
*/
package com.android.microdroid.test.device;
-import static com.google.common.truth.TruthJUnit.assume;
+import static android.content.pm.PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK;
-import static org.junit.Assume.assumeNoException;
+import static com.google.common.truth.TruthJUnit.assume;
import android.app.Instrumentation;
import android.app.UiAutomation;
@@ -35,8 +35,8 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.microdroid.test.common.DeviceProperties;
import com.android.microdroid.test.common.MetricsProcessor;
-import com.android.virt.VirtualizationTestHelper;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
@@ -51,13 +51,12 @@
public abstract class MicrodroidDeviceTestBase {
public static boolean isCuttlefish() {
- return VirtualizationTestHelper.isCuttlefish(
- SystemProperties.get("ro.product.vendor.device"));
+ return DeviceProperties.create(SystemProperties::get).isCuttlefish();
}
public static String getMetricPrefix() {
return MetricsProcessor.getMetricPrefix(
- SystemProperties.get("debug.hypervisor.metrics_tag"));
+ DeviceProperties.create(SystemProperties::get).getMetricsTag());
}
protected final void grantPermission(String permission) {
@@ -119,17 +118,12 @@
}
public void prepareTestSetup(boolean protectedVm) {
- // In case when the virt APEX doesn't exist on the device, classes in the
- // android.system.virtualmachine package can't be loaded. Therefore, before using the
- // classes, check the existence of a class in the package and skip this test if not exist.
- try {
- Class.forName("android.system.virtualmachine.VirtualMachineManager");
- } catch (ClassNotFoundException e) {
- assumeNoException(e);
- return;
- }
- Context context = ApplicationProvider.getApplicationContext();
- mInner = new Inner(context, protectedVm, VirtualMachineManager.getInstance(context));
+ Context ctx = ApplicationProvider.getApplicationContext();
+ assume().withMessage("Device doesn't support AVF")
+ .that(ctx.getPackageManager().hasSystemFeature(FEATURE_VIRTUALIZATION_FRAMEWORK))
+ .isTrue();
+
+ mInner = new Inner(ctx, protectedVm, VirtualMachineManager.getInstance(ctx));
int capabilities = mInner.getVirtualMachineManager().getCapabilities();
if (protectedVm) {
@@ -232,7 +226,7 @@
}
@Override
- public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {}
+ public void onPayloadStarted(VirtualMachine vm) {}
@Override
public void onPayloadReady(VirtualMachine vm) {}
@@ -327,7 +321,7 @@
VmEventListener listener =
new VmEventListener() {
@Override
- public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {
+ public void onPayloadStarted(VirtualMachine vm) {
endTime.complete(System.nanoTime());
payloadStarted.complete(true);
forceStop(vm);
diff --git a/tests/hostside/helper/Android.bp b/tests/hostside/helper/Android.bp
index b2333ab..6196ec5 100644
--- a/tests/hostside/helper/Android.bp
+++ b/tests/hostside/helper/Android.bp
@@ -12,6 +12,5 @@
],
static_libs: [
"MicrodroidTestHelper",
- "VirtualizationTestHelper",
],
}
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
index 43fe615..b4b3795 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
@@ -26,6 +26,7 @@
import static org.junit.Assume.assumeTrue;
import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.microdroid.test.common.DeviceProperties;
import com.android.microdroid.test.common.MetricsProcessor;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.device.DeviceNotAvailableException;
@@ -34,7 +35,6 @@
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.RunUtil;
-import com.android.virt.VirtualizationTestHelper;
import java.io.File;
import java.io.FileNotFoundException;
@@ -49,10 +49,6 @@
private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
private static final String INSTANCE_IMG = "instance.img";
- // This is really slow on GCE (2m 40s) but fast on localhost or actual Android phones (< 10s).
- // Then there is time to run the actual task. Set the maximum timeout value big enough.
- private static final long MICRODROID_MAX_LIFETIME_MINUTES = 20;
-
private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
protected static final long MICRODROID_COMMAND_TIMEOUT_MILLIS = 30000;
private static final long MICRODROID_COMMAND_RETRY_INTERVAL_MILLIS = 500;
@@ -87,19 +83,21 @@
android.tryRun("rm", "-rf", "/data/misc/virtualizationservice/*");
}
- protected boolean isCuttlefish() throws Exception {
- return VirtualizationTestHelper.isCuttlefish(
- getDevice().getProperty("ro.product.vendor.device"));
+ protected boolean isCuttlefish() {
+ return DeviceProperties.create(getDevice()::getProperty).isCuttlefish();
}
- protected String getMetricPrefix() throws Exception {
+ protected String getMetricPrefix() {
return MetricsProcessor.getMetricPrefix(
- getDevice().getProperty("debug.hypervisor.metrics_tag"));
+ DeviceProperties.create(getDevice()::getProperty).getMetricsTag());
}
public static void testIfDeviceIsCapable(ITestDevice androidDevice) throws Exception {
assumeTrue("Need an actual TestDevice", androidDevice instanceof TestDevice);
TestDevice testDevice = (TestDevice) androidDevice;
+ assumeTrue(
+ "Requires VM support",
+ testDevice.hasFeature("android.software.virtualization_framework"));
assumeTrue("Requires VM support", testDevice.supportsMicrodroid());
}
@@ -193,17 +191,6 @@
return pathLine.substring("package:".length());
}
- private static void forwardFileToLog(CommandRunner android, String path, String tag)
- throws DeviceNotAvailableException {
- android.runWithTimeout(
- MICRODROID_MAX_LIFETIME_MINUTES * 60 * 1000,
- "logwrapper",
- "sh",
- "-c",
- "\"$'tail -f -n +0 " + path
- + " | sed \\'s/^/" + tag + ": /g\\''\""); // add tags in front of lines
- }
-
public static void shutdownMicrodroid(ITestDevice androidDevice, String cid)
throws DeviceNotAvailableException {
CommandRunner android = new CommandRunner(androidDevice);
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index a836559..4e9c501 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -42,6 +42,7 @@
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.TestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
import com.android.tradefed.util.CommandResult;
@@ -62,13 +63,19 @@
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
+import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -266,11 +273,11 @@
return new ActiveApexInfoList(list);
}
- private String runMicrodroidWithResignedImages(
+ private Process runMicrodroidWithResignedImages(
File key,
Map<String, File> keyOverrides,
boolean isProtected,
- boolean daemonize,
+ boolean waitForOnline,
String consolePath)
throws Exception {
CommandRunner android = new CommandRunner(getDevice());
@@ -375,19 +382,49 @@
final String configPath = TEST_ROOT + "raw_config.json";
getDevice().pushString(config.toString(), configPath);
- final String logPath = LOG_PATH;
- final String ret =
- android.runWithTimeout(
- 60 * 1000,
+ List<String> args =
+ Arrays.asList(
+ "adb",
+ "-s",
+ getDevice().getSerialNumber(),
+ "shell",
VIRT_APEX + "bin/vm run",
- daemonize ? "--daemonize" : "",
(consolePath != null) ? "--console " + consolePath : "",
- "--log " + logPath,
+ "--log " + LOG_PATH,
configPath);
+
+ PipedInputStream pis = new PipedInputStream();
+ Process process = RunUtil.getDefault().runCmdInBackground(args, new PipedOutputStream(pis));
+ BufferedReader stdout = new BufferedReader(new InputStreamReader(pis));
+
+ // Retrieve the CID from the vm tool output
+ String cid = null;
Pattern pattern = Pattern.compile("with CID (\\d+)");
- Matcher matcher = pattern.matcher(ret);
- assertWithMessage("Failed to find CID").that(matcher.find()).isTrue();
- return matcher.group(1);
+ try {
+ String line;
+ while ((line = stdout.readLine()) != null) {
+ CLog.i("VM output: " + line);
+ Matcher matcher = pattern.matcher(line);
+ if (matcher.find()) {
+ cid = matcher.group(1);
+ break;
+ }
+ }
+ } catch (IOException ex) {
+ throw new IllegalStateException(
+ "Could not find the CID of the VM. The process probably died.", ex);
+ }
+ if (cid == null) {
+ throw new IllegalStateException(
+ "Could not find the CID of the VM. Output does not contain the expected"
+ + " pattern.");
+ }
+
+ if (waitForOnline) {
+ adbConnectToMicrodroid(getDevice(), cid);
+ }
+
+ return process;
}
@Test
@@ -400,9 +437,12 @@
File key = findTestFile("test.com.android.virt.pem");
Map<String, File> keyOverrides = Map.of();
boolean isProtected = true;
- boolean daemonize = false; // VM should shut down due to boot failure.
+ boolean waitForOnline = false; // VM should shut down due to boot failure.
String consolePath = TEST_ROOT + "console";
- runMicrodroidWithResignedImages(key, keyOverrides, isProtected, daemonize, consolePath);
+ Process process =
+ runMicrodroidWithResignedImages(
+ key, keyOverrides, isProtected, waitForOnline, consolePath);
+ process.waitFor(5L, TimeUnit.SECONDS);
assertThat(getDevice().pullFileContents(consolePath), containsString("pvmfw boot failed"));
}
@@ -416,14 +456,12 @@
File key = findTestFile("test.com.android.virt.pem");
Map<String, File> keyOverrides = Map.of();
boolean isProtected = false;
- boolean daemonize = true;
+ boolean waitForOnline = true; // Device online means that boot must have succeeded.
String consolePath = TEST_ROOT + "console";
- String cid =
+ Process process =
runMicrodroidWithResignedImages(
- key, keyOverrides, isProtected, daemonize, consolePath);
- // Adb connection to the microdroid means that boot succeeded.
- adbConnectToMicrodroid(getDevice(), cid);
- shutdownMicrodroid(getDevice(), cid);
+ key, keyOverrides, isProtected, waitForOnline, consolePath);
+ process.destroy();
}
@Test
@@ -434,18 +472,18 @@
File key2 = findTestFile("test2.com.android.virt.pem");
Map<String, File> keyOverrides = Map.of("microdroid_vbmeta.img", key2);
boolean isProtected = false; // Not interested in pvwfw
- boolean daemonize = true; // Bootloader fails and enters prompts.
+ boolean waitForOnline = false; // Bootloader fails and enters prompts.
// To be able to stop it, it should be a daemon.
String consolePath = TEST_ROOT + "console";
- String cid =
+ Process process =
runMicrodroidWithResignedImages(
- key, keyOverrides, isProtected, daemonize, consolePath);
+ key, keyOverrides, isProtected, waitForOnline, consolePath);
// Wait so that init can print errors to console (time in cuttlefish >> in real device)
assertThatEventually(
100000,
() -> getDevice().pullFileContents(consolePath),
containsString("init: [libfs_avb]Failed to verify vbmeta digest"));
- shutdownMicrodroid(getDevice(), cid);
+ process.destroy();
}
private boolean isTombstoneGeneratedWithConfig(String configPath) throws Exception {
@@ -602,6 +640,7 @@
// Check that no denials have happened so far
CommandRunner android = new CommandRunner(getDevice());
assertThat(android.tryRun("egrep", "'avc:[[:space:]]{1,2}denied'", LOG_PATH)).isNull();
+ assertThat(android.tryRun("egrep", "'avc:[[:space:]]{1,2}denied'", CONSOLE_PATH)).isNull();
assertThat(microdroid.run("cat /proc/cpuinfo | grep processor | wc -l"))
.isEqualTo(Integer.toString(NUM_VCPUS));
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 8972046..707dca1 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -19,7 +19,10 @@
"truth-prebuilt",
"compatibility-common-util-devicesidelib",
],
- libs: ["android.system.virtualmachine"],
+ // We need to compile against the .impl library which includes the hidden
+ // APIs. Once the APIs are promoted to @SystemApi we can switch to
+ // framework-virtualization, which contains API stubs.
+ libs: ["framework-virtualization.impl"],
jni_libs: [
"MicrodroidTestNativeLib",
"MicrodroidIdleNativeLib",
@@ -55,3 +58,10 @@
srcs: ["src/native/testlib.cpp"],
stl: "libc++_static",
}
+
+cc_library_shared {
+ name: "MicrodroidIdleNativeLib",
+ srcs: ["src/native/idlebinary.cpp"],
+ header_libs: ["vm_payload_headers"],
+ stl: "libc++_static",
+}
diff --git a/tests/testapk/AndroidManifest.xml b/tests/testapk/AndroidManifest.xml
index ab22546..fefd20a 100644
--- a/tests/testapk/AndroidManifest.xml
+++ b/tests/testapk/AndroidManifest.xml
@@ -18,8 +18,8 @@
<uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
<uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
<uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
+ <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
<application>
- <uses-library android:name="android.system.virtualmachine" android:required="false" />
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.microdroid.test"
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 0e9ba55..da7c7ec 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -27,7 +27,6 @@
import android.content.Context;
import android.os.Build;
-import android.os.ParcelFileDescriptor;
import android.os.ServiceSpecificException;
import android.os.SystemProperties;
import android.system.virtualmachine.VirtualMachine;
@@ -61,6 +60,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Arrays;
import java.util.List;
import java.util.OptionalLong;
import java.util.UUID;
@@ -121,6 +121,7 @@
assertThat(testResults.mAppRunProp).isEqualTo("true");
assertThat(testResults.mSublibRunProp).isEqualTo("true");
assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
+ assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
}
@Test
@@ -600,31 +601,77 @@
}
@Test
- public void vmConvertsToValidDescriptor() throws Exception {
+ public void importedVmAndOriginalVmHaveTheSameCdi() throws Exception {
+ assumeSupportedKernel();
+ // Arrange
+ grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+ VirtualMachineConfig config =
+ mInner.newVmConfigBuilder()
+ .setPayloadConfigPath("assets/vm_config.json")
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .build();
+ String vmNameOrig = "test_vm_orig";
+ String vmNameImport = "test_vm_import";
+ VirtualMachine vmOrig = mInner.forceCreateNewVirtualMachine(vmNameOrig, config);
+ VmCdis origCdis = launchVmAndGetCdis(vmNameOrig);
+ assertThat(origCdis.instanceSecret).isNotNull();
+ VirtualMachineDescriptor descriptor = vmOrig.toDescriptor();
+ VirtualMachineManager vmm = mInner.getVirtualMachineManager();
+ if (vmm.get(vmNameImport) != null) {
+ vmm.delete(vmNameImport);
+ }
+
+ // Action
+ // The imported VM will be fetched by name later.
+ VirtualMachine unusedVmImport = vmm.importFromDescriptor(vmNameImport, descriptor);
+
+ // Asserts
+ VmCdis importCdis = launchVmAndGetCdis(vmNameImport);
+ assertThat(origCdis.instanceSecret).isEqualTo(importCdis.instanceSecret);
+ }
+
+ @Test
+ public void importedVmIsEqualToTheOriginalVm() throws Exception {
// Arrange
VirtualMachineConfig config =
mInner.newVmConfigBuilder()
.setPayloadBinaryPath("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_NONE)
.build();
- String vmName = "test_vm";
- VirtualMachine vm = mInner.forceCreateNewVirtualMachine(vmName, config);
+ String vmNameOrig = "test_vm_orig";
+ String vmNameImport = "test_vm_import";
+ VirtualMachine vmOrig = mInner.forceCreateNewVirtualMachine(vmNameOrig, config);
+ // Run something to make the instance.img different with the initialized one.
+ TestResults origTestResults = runVmTestService(vmOrig);
+ assertThat(origTestResults.mException).isNull();
+ assertThat(origTestResults.mAddInteger).isEqualTo(123 + 456);
+ VirtualMachineDescriptor descriptor = vmOrig.toDescriptor();
+ VirtualMachineManager vmm = mInner.getVirtualMachineManager();
+ if (vmm.get(vmNameImport) != null) {
+ vmm.delete(vmNameImport);
+ }
// Action
- VirtualMachineDescriptor descriptor = vm.toDescriptor();
+ VirtualMachine vmImport = vmm.importFromDescriptor(vmNameImport, descriptor);
// Asserts
- assertFileContentsAreEqual(descriptor.getConfigFd(), vmName, "config.xml");
- assertFileContentsAreEqual(descriptor.getInstanceImgFd(), vmName, "instance.img");
+ assertFileContentsAreEqualInTwoVms("config.xml", vmNameOrig, vmNameImport);
+ assertFileContentsAreEqualInTwoVms("instance.img", vmNameOrig, vmNameImport);
+ assertThat(vmImport).isNotEqualTo(vmOrig);
+ vmm.delete(vmNameOrig);
+ assertThat(vmImport).isEqualTo(vmm.get(vmNameImport));
+ TestResults testResults = runVmTestService(vmImport);
+ assertThat(testResults.mException).isNull();
+ assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
}
- private void assertFileContentsAreEqual(
- ParcelFileDescriptor parcelFd, String vmName, String fileName) throws IOException {
- File file = getVmFile(vmName, fileName);
- // Use try-with-resources to close the files automatically after assert.
- try (FileInputStream input1 = new FileInputStream(parcelFd.getFileDescriptor());
- FileInputStream input2 = new FileInputStream(file)) {
- assertThat(input1.readAllBytes()).isEqualTo(input2.readAllBytes());
+ private void assertFileContentsAreEqualInTwoVms(String fileName, String vmName1, String vmName2)
+ throws IOException {
+ File file1 = getVmFile(vmName1, fileName);
+ File file2 = getVmFile(vmName2, fileName);
+ try (FileInputStream input1 = new FileInputStream(file1);
+ FileInputStream input2 = new FileInputStream(file2)) {
+ assertThat(Arrays.equals(input1.readAllBytes(), input2.readAllBytes())).isTrue();
}
}
@@ -661,6 +708,7 @@
String mSublibRunProp;
String mExtraApkTestProp;
String mApkContentsPath;
+ String mEncryptedStoragePath;
}
private TestResults runVmTestService(VirtualMachine vm) throws Exception {
@@ -671,8 +719,9 @@
new VmEventListener() {
private void testVMService(VirtualMachine vm) {
try {
- ITestService testService = ITestService.Stub.asInterface(
- vm.connectToVsockServer(ITestService.SERVICE_PORT));
+ ITestService testService =
+ ITestService.Stub.asInterface(
+ vm.connectToVsockServer(ITestService.SERVICE_PORT));
testResults.mAddInteger = testService.addInteger(123, 456);
testResults.mAppRunProp =
testService.readProperty("debug.microdroid.app.run");
@@ -681,6 +730,8 @@
testResults.mExtraApkTestProp =
testService.readProperty("debug.microdroid.test.extra_apk");
testResults.mApkContentsPath = testService.getApkContentsPath();
+ testResults.mEncryptedStoragePath =
+ testService.getEncryptedStoragePath();
} catch (Exception e) {
testResults.mException = e;
}
@@ -695,11 +746,9 @@
}
@Override
- public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {
+ public void onPayloadStarted(VirtualMachine vm) {
Log.i(TAG, "onPayloadStarted");
payloadStarted.complete(true);
- logVmOutput(TAG, new FileInputStream(stream.getFileDescriptor()),
- "Payload");
}
};
listener.runToFinish(TAG, vm);
diff --git a/tests/benchmark/src/native/idlebinary.cpp b/tests/testapk/src/native/idlebinary.cpp
similarity index 100%
rename from tests/benchmark/src/native/idlebinary.cpp
rename to tests/testapk/src/native/idlebinary.cpp
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 48942dc..694f452 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -123,6 +123,16 @@
*out = path;
return ndk::ScopedAStatus::ok();
}
+
+ ndk::ScopedAStatus getEncryptedStoragePath(std::string* out) override {
+ const char* path_c = AVmPayload_getEncryptedStoragePath();
+ if (path_c == nullptr) {
+ out->clear();
+ } else {
+ *out = path_c;
+ }
+ return ndk::ScopedAStatus::ok();
+ }
};
auto testService = ndk::SharedRefBase::make<TestService>();
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index d6f4607..b767013 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -22,6 +22,7 @@
rustlibs: [
"android.system.virtualizationcommon-rust",
"android.system.virtualizationservice-rust",
+ "android.system.virtualizationservice_internal-rust",
"android.system.virtualmachineservice-rust",
"android.os.permissions_aidl-rust",
"libandroid_logger",
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index 4d5326a..a0bbc00 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -34,11 +34,31 @@
}
aidl_interface {
+ name: "android.system.virtualizationservice_internal",
+ srcs: ["android/system/virtualizationservice_internal/**/*.aidl"],
+ unstable: true,
+ backend: {
+ java: {
+ sdk_version: "module_current",
+ },
+ rust: {
+ enabled: true,
+ apex_available: [
+ "com.android.virt",
+ ],
+ },
+ },
+}
+
+aidl_interface {
name: "android.system.virtualmachineservice",
srcs: ["android/system/virtualmachineservice/**/*.aidl"],
imports: ["android.system.virtualizationcommon"],
unstable: true,
backend: {
+ java: {
+ sdk_version: "module_current",
+ },
rust: {
enabled: true,
apex_available: [
@@ -55,6 +75,7 @@
unstable: true,
backend: {
java: {
+ sdk_version: "module_current",
apex_available: ["com.android.virt"],
},
ndk: {
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
index 8d6ed08..a329fa6 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
@@ -24,13 +24,9 @@
*/
oneway interface IVirtualMachineCallback {
/**
- * Called when the payload starts in the VM. `stream` is the input/output port of the payload.
- *
- * <p>Note: when the virtual machine object is shared to multiple processes and they register
- * this callback to the same virtual machine object, the processes will compete to access the
- * same payload stream. Keep only one process to access the stream.
+ * Called when the payload starts in the VM.
*/
- void onPayloadStarted(int cid, in @nullable ParcelFileDescriptor stream);
+ void onPayloadStarted(int cid);
/**
* Called when the payload in the VM is ready to serve.
diff --git a/tests/helper/src/java/com/android/virt/VirtualizationTestHelper.java b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
similarity index 66%
rename from tests/helper/src/java/com/android/virt/VirtualizationTestHelper.java
rename to virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
index 4c27915..1a7aa4a 100644
--- a/tests/helper/src/java/com/android/virt/VirtualizationTestHelper.java
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright 2022 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.
@@ -13,10 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.virt;
+package android.system.virtualizationservice_internal;
-public abstract class VirtualizationTestHelper {
- public static boolean isCuttlefish(String vendorDeviceName) {
- return vendorDeviceName != null && vendorDeviceName.startsWith("vsoc_");
- }
+interface IGlobalVmContext {
+ /** Get the CID allocated to the VM. */
+ int getCid();
}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
new file mode 100644
index 0000000..851ddf4
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2022 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.virtualizationservice_internal;
+
+import android.system.virtualizationservice_internal.IGlobalVmContext;
+
+interface IVirtualizationServiceInternal {
+ /**
+ * Allocates global context for a new VM.
+ *
+ * This allocates VM's globally unique resources such as the CID.
+ * The resources will not be recycled as long as there is a strong reference
+ * to the returned object.
+ */
+ IGlobalVmContext allocateGlobalVmContext();
+}
diff --git a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
index e8c1724..f2d92af 100644
--- a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
@@ -21,12 +21,6 @@
interface IVirtualMachineService {
/**
* Port number that VirtualMachineService listens on connections from the guest VMs for the
- * payload input and output.
- */
- const int VM_STREAM_SERVICE_PORT = 3000;
-
- /**
- * Port number that VirtualMachineService listens on connections from the guest VMs for the
* VirtualMachineService binder service.
*/
const int VM_BINDER_SERVICE_PORT = 5000;
@@ -53,7 +47,7 @@
void notifyPayloadFinished(int exitCode);
/**
- * Notifies that an error has occurred inside the VM..
+ * Notifies that an error has occurred inside the VM.
*/
void notifyError(ErrorCode errorCode, in String message);
}
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index bc697e3..cfcfa2b 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -36,9 +36,13 @@
VirtualMachineRawConfig::VirtualMachineRawConfig,
VirtualMachineState::VirtualMachineState,
};
+use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::{
+ IGlobalVmContext::{BnGlobalVmContext, IGlobalVmContext},
+ IVirtualizationServiceInternal::{BnVirtualizationServiceInternal, IVirtualizationServiceInternal},
+};
use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
BnVirtualMachineService, IVirtualMachineService, VM_BINDER_SERVICE_PORT,
- VM_STREAM_SERVICE_PORT, VM_TOMBSTONES_SERVICE_PORT,
+ VM_TOMBSTONES_SERVICE_PORT,
};
use anyhow::{anyhow, bail, Context, Result};
use apkverify::{HashAlgorithm, V4Signature};
@@ -94,10 +98,94 @@
const MICRODROID_OS_NAME: &str = "microdroid";
-/// Implementation of `IVirtualizationService`, the entry point of the AIDL service.
+/// Singleton service for allocating globally-unique VM resources, such as the CID, and running
+/// singleton servers, like tombstone receiver.
#[derive(Debug, Default)]
+pub struct VirtualizationServiceInternal {
+ state: Arc<Mutex<GlobalState>>,
+}
+
+impl VirtualizationServiceInternal {
+ pub fn init() -> VirtualizationServiceInternal {
+ let service = VirtualizationServiceInternal::default();
+
+ std::thread::spawn(|| {
+ if let Err(e) = handle_stream_connection_tombstoned() {
+ warn!("Error receiving tombstone from guest or writing them. Error: {:?}", e);
+ }
+ });
+
+ service
+ }
+}
+
+impl Interface for VirtualizationServiceInternal {}
+
+impl IVirtualizationServiceInternal for VirtualizationServiceInternal {
+ fn allocateGlobalVmContext(&self) -> binder::Result<Strong<dyn IGlobalVmContext>> {
+ let state = &mut *self.state.lock().unwrap();
+ let cid = state.allocate_cid().map_err(|e| {
+ Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some(e.to_string()))
+ })?;
+ Ok(GlobalVmContext::create(cid))
+ }
+}
+
+/// The mutable state of the VirtualizationServiceInternal. There should only be one instance
+/// of this struct.
+#[derive(Debug, Default)]
+struct GlobalState {}
+
+impl GlobalState {
+ /// Get the next available CID, or an error if we have run out. The last CID used is stored in
+ /// a system property so that restart of virtualizationservice doesn't reuse CID while the host
+ /// Android is up.
+ fn allocate_cid(&mut self) -> Result<Cid> {
+ let cid = match system_properties::read(SYSPROP_LAST_CID)? {
+ Some(val) => match val.parse::<Cid>() {
+ Ok(num) => num.checked_add(1).ok_or_else(|| anyhow!("ran out of CIDs"))?,
+ Err(_) => {
+ error!("Invalid value '{}' of property '{}'", val, SYSPROP_LAST_CID);
+ FIRST_GUEST_CID
+ }
+ },
+ None => FIRST_GUEST_CID,
+ };
+ system_properties::write(SYSPROP_LAST_CID, &format!("{}", cid))?;
+ Ok(cid)
+ }
+}
+
+/// Implementation of the AIDL `IGlobalVmContext` interface.
+#[derive(Debug, Default)]
+struct GlobalVmContext {
+ /// The unique CID assigned to the VM for vsock communication.
+ cid: Cid,
+ /// Keeps our service process running as long as this VM instance exists.
+ #[allow(dead_code)]
+ lazy_service_guard: LazyServiceGuard,
+}
+
+impl GlobalVmContext {
+ fn create(cid: Cid) -> Strong<dyn IGlobalVmContext> {
+ let binder = GlobalVmContext { cid, ..Default::default() };
+ BnGlobalVmContext::new_binder(binder, BinderFeatures::default())
+ }
+}
+
+impl Interface for GlobalVmContext {}
+
+impl IGlobalVmContext for GlobalVmContext {
+ fn getCid(&self) -> binder::Result<i32> {
+ Ok(self.cid as i32)
+ }
+}
+
+/// Implementation of `IVirtualizationService`, the entry point of the AIDL service.
+#[derive(Debug)]
pub struct VirtualizationService {
state: Arc<Mutex<State>>,
+ global_service: Strong<dyn IVirtualizationServiceInternal>,
}
impl Interface for VirtualizationService {
@@ -299,19 +387,11 @@
impl VirtualizationService {
pub fn init() -> VirtualizationService {
- let service = VirtualizationService::default();
+ let global_service = VirtualizationServiceInternal::init();
+ let global_service =
+ BnVirtualizationServiceInternal::new_binder(global_service, BinderFeatures::default());
- // server for payload output
- let state = service.state.clone(); // reference to state (not the state itself) is copied
- std::thread::spawn(move || {
- handle_stream_connection_from_vm(state).unwrap();
- });
-
- std::thread::spawn(|| {
- if let Err(e) = handle_stream_connection_tombstoned() {
- warn!("Error receiving tombstone from guest or writing them. Error: {:?}", e);
- }
- });
+ let service = VirtualizationService { global_service, state: Default::default() };
// binder server for vm
// reference to state (not the state itself) is copied
@@ -352,12 +432,14 @@
check_use_custom_virtual_machine()?;
}
+ let vm_context = self.global_service.allocateGlobalVmContext()?;
+ let cid = vm_context.getCid()? as Cid;
+
let state = &mut *self.state.lock().unwrap();
let console_fd = console_fd.map(clone_file).transpose()?;
let log_fd = log_fd.map(clone_file).transpose()?;
let requester_uid = ThreadState::get_calling_uid();
let requester_debug_pid = ThreadState::get_calling_pid();
- let cid = state.next_cid().or(Err(ExceptionCode::ILLEGAL_STATE))?;
// Counter to generate unique IDs for temporary image files.
let mut next_temporary_image_id = 0;
@@ -474,47 +556,26 @@
detect_hangup: is_app_config,
};
let instance = Arc::new(
- VmInstance::new(crosvm_config, temporary_directory, requester_uid, requester_debug_pid)
- .map_err(|e| {
- error!("Failed to create VM with config {:?}: {:?}", config, e);
- Status::new_service_specific_error_str(
- -1,
- Some(format!("Failed to create VM: {:?}", e)),
- )
- })?,
+ VmInstance::new(
+ crosvm_config,
+ temporary_directory,
+ requester_uid,
+ requester_debug_pid,
+ vm_context,
+ )
+ .map_err(|e| {
+ error!("Failed to create VM with config {:?}: {:?}", config, e);
+ Status::new_service_specific_error_str(
+ -1,
+ Some(format!("Failed to create VM: {:?}", e)),
+ )
+ })?,
);
state.add_vm(Arc::downgrade(&instance));
Ok(VirtualMachine::create(instance))
}
}
-/// Waits for incoming connections from VM. If a new connection is made, stores the stream in the
-/// corresponding `VmInstance`.
-fn handle_stream_connection_from_vm(state: Arc<Mutex<State>>) -> Result<()> {
- let listener =
- VsockListener::bind_with_cid_port(VMADDR_CID_HOST, VM_STREAM_SERVICE_PORT as u32)?;
- for stream in listener.incoming() {
- let stream = match stream {
- Err(e) => {
- warn!("invalid incoming connection: {:?}", e);
- continue;
- }
- Ok(s) => s,
- };
- if let Ok(addr) = stream.peer_addr() {
- let cid = addr.cid();
- let port = addr.port();
- info!("payload stream connected from cid={}, port={}", cid, port);
- if let Some(vm) = state.lock().unwrap().get_vm(cid) {
- *vm.stream.lock().unwrap() = Some(stream);
- } else {
- error!("connection from cid={} is not from a guest VM", cid);
- }
- }
- }
- Ok(())
-}
-
fn write_zero_filler(zero_filler_path: &Path) -> Result<()> {
let file = OpenOptions::new()
.create_new(true)
@@ -533,8 +594,7 @@
}
fn prepare_ramdump_file(ramdump_path: &Path) -> Result<File> {
- File::create(&ramdump_path)
- .context(format!("Failed to create ramdump file {:?}", &ramdump_path))
+ File::create(ramdump_path).context(format!("Failed to create ramdump file {:?}", &ramdump_path))
}
/// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
@@ -854,11 +914,10 @@
impl VirtualMachineCallbacks {
/// Call all registered callbacks to notify that the payload has started.
- pub fn notify_payload_started(&self, cid: Cid, stream: Option<VsockStream>) {
+ pub fn notify_payload_started(&self, cid: Cid) {
let callbacks = &*self.0.lock().unwrap();
- let pfd = stream.map(vsock_stream_to_pfd);
for callback in callbacks {
- if let Err(e) = callback.onPayloadStarted(cid as i32, pfd.as_ref()) {
+ if let Err(e) = callback.onPayloadStarted(cid as i32) {
error!("Error notifying payload start event from VM CID {}: {:?}", cid, e);
}
}
@@ -968,27 +1027,6 @@
let vm = self.debug_held_vms.swap_remove(pos);
Some(vm)
}
-
- /// Get the next available CID, or an error if we have run out. The last CID used is stored in
- /// a system property so that restart of virtualizationservice doesn't reuse CID while the host
- /// Android is up.
- fn next_cid(&mut self) -> Result<Cid> {
- let next = if let Some(val) = system_properties::read(SYSPROP_LAST_CID)? {
- if let Ok(num) = val.parse::<u32>() {
- num.checked_add(1).ok_or_else(|| anyhow!("run out of CID"))?
- } else {
- error!("Invalid last CID {}. Using {}", &val, FIRST_GUEST_CID);
- FIRST_GUEST_CID
- }
- } else {
- // First VM since the boot
- FIRST_GUEST_CID
- };
- // Persist the last value for next use
- let str_val = format!("{}", next);
- system_properties::write(SYSPROP_LAST_CID, &str_val)?;
- Ok(next)
- }
}
/// Gets the `VirtualMachineState` of the given `VmInstance`.
@@ -1072,11 +1110,10 @@
vm.update_payload_state(PayloadState::Started).map_err(|e| {
Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some(e.to_string()))
})?;
- let stream = vm.stream.lock().unwrap().take();
- vm.callbacks.notify_payload_started(cid, stream);
+ vm.callbacks.notify_payload_started(cid);
- let vm_start_timestamp = vm.vm_start_timestamp.lock().unwrap();
- write_vm_booted_stats(vm.requester_uid as i32, &vm.name, *vm_start_timestamp);
+ let vm_start_timestamp = vm.vm_metric.lock().unwrap().start_timestamp;
+ write_vm_booted_stats(vm.requester_uid as i32, &vm.name, vm_start_timestamp);
Ok(())
} else {
error!("notifyPayloadStarted is called from an unknown CID {}", cid);
diff --git a/virtualizationservice/src/atom.rs b/virtualizationservice/src/atom.rs
index 20f88e7..9c74d1e 100644
--- a/virtualizationservice/src/atom.rs
+++ b/virtualizationservice/src/atom.rs
@@ -15,6 +15,7 @@
//! Functions for creating and collecting atoms.
use crate::aidl::clone_file;
+use crate::crosvm::VmMetric;
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
DeathReason::DeathReason,
IVirtualMachine::IVirtualMachine,
@@ -164,15 +165,18 @@
uid: i32,
vm_identifier: &str,
reason: DeathReason,
- vm_start_timestamp: Option<SystemTime>,
+ vm_metric: &VmMetric,
) {
let vm_identifier = vm_identifier.to_owned();
- let duration = get_duration(vm_start_timestamp);
+ let elapsed_time_millis = get_duration(vm_metric.start_timestamp).as_millis() as i64;
+ let guest_time_millis = vm_metric.cpu_guest_time.unwrap_or_default();
+ let rss = vm_metric.rss.unwrap_or_default();
+
thread::spawn(move || {
let vm_exited = vm_exited::VmExited {
uid,
vm_identifier: &vm_identifier,
- elapsed_time_millis: duration.as_millis() as i64,
+ elapsed_time_millis,
death_reason: match reason {
DeathReason::INFRASTRUCTURE_ERROR => vm_exited::DeathReason::InfrastructureError,
DeathReason::KILLED => vm_exited::DeathReason::Killed,
@@ -211,6 +215,9 @@
DeathReason::HANGUP => vm_exited::DeathReason::Hangup,
_ => vm_exited::DeathReason::Unknown,
},
+ guest_time_millis,
+ rss_vm_kb: rss.vm,
+ rss_crosvm_kb: rss.crosvm,
};
wait_for_statsd().unwrap_or_else(|e| warn!("failed to wait for statsd with error: {}", e));
match vm_exited.stats_write() {
diff --git a/virtualizationservice/src/composite.rs b/virtualizationservice/src/composite.rs
index c9a68ac..fe17ff4 100644
--- a/virtualizationservice/src/composite.rs
+++ b/virtualizationservice/src/composite.rs
@@ -51,7 +51,7 @@
OpenOptions::new().create_new(true).read(true).write(true).open(footer_path).with_context(
|| format!("Failed to create composite image header {:?}", footer_path),
)?;
- let zero_filler_file = File::open(&zero_filler_path).with_context(|| {
+ let zero_filler_file = File::open(zero_filler_path).with_context(|| {
format!("Failed to open composite image zero filler {:?}", zero_filler_path)
})?;
@@ -66,7 +66,7 @@
)?;
// Re-open the composite image as read-only.
- let composite_image = File::open(&output_path)
+ let composite_image = File::open(output_path)
.with_context(|| format!("Failed to open composite image {:?}", output_path))?;
files.push(header_file);
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index 1b8061e..68324c5 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -16,16 +16,18 @@
use crate::aidl::{Cid, VirtualMachineCallbacks};
use crate::atom::write_vm_exited_stats;
-use anyhow::{anyhow, bail, Context, Error};
+use anyhow::{anyhow, bail, Context, Error, Result};
use command_fds::CommandFdExt;
use lazy_static::lazy_static;
+use libc::{sysconf, _SC_CLK_TCK};
use log::{debug, error, info};
use semver::{Version, VersionReq};
use nix::{fcntl::OFlag, unistd::pipe2};
use regex::{Captures, Regex};
use shared_child::SharedChild;
use std::borrow::Cow;
-use std::fs::{remove_dir_all, File};
+use std::cmp::max;
+use std::fs::{read_to_string, remove_dir_all, File};
use std::io::{self, Read};
use std::mem;
use std::num::NonZeroU32;
@@ -35,8 +37,8 @@
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, SystemTime};
use std::thread;
-use vsock::VsockStream;
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::DeathReason::DeathReason;
+use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
use binder::Strong;
use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
use tombstoned_client::{TombstonedConnection, DebuggerdDumpType};
@@ -61,6 +63,8 @@
/// The exit status which crosvm returns when vcpu is stalled.
const CROSVM_WATCHDOG_REBOOT_STATUS: i32 = 36;
+const MILLIS_PER_SEC: i64 = 1000;
+
lazy_static! {
/// If the VM doesn't move to the Started state within this amount time, a hang-up error is
/// triggered.
@@ -133,6 +137,24 @@
Failed,
}
+/// RSS values of VM and CrosVM process itself.
+#[derive(Copy, Clone, Debug, Default)]
+pub struct Rss {
+ pub vm: i64,
+ pub crosvm: i64,
+}
+
+/// Metrics regarding the VM.
+#[derive(Debug, Default)]
+pub struct VmMetric {
+ /// Recorded timestamp when the VM is started.
+ pub start_timestamp: Option<SystemTime>,
+ /// Update most recent guest_time periodically from /proc/[crosvm pid]/stat while VM is running.
+ pub cpu_guest_time: Option<i64>,
+ /// Update maximum RSS values periodically from /proc/[crosvm pid]/smaps while VM is running.
+ pub rss: Option<Rss>,
+}
+
impl VmState {
/// Tries to start the VM, if it is in the `NotStarted` state.
///
@@ -147,6 +169,12 @@
let child =
Arc::new(run_vm(config, &instance.temporary_directory, failure_pipe_write)?);
+ let instance_monitor_status = instance.clone();
+ let child_monitor_status = child.clone();
+ thread::spawn(move || {
+ instance_monitor_status.clone().monitor_vm_status(child_monitor_status);
+ });
+
let child_clone = child.clone();
let instance_clone = instance.clone();
thread::spawn(move || {
@@ -175,6 +203,9 @@
pub struct VmInstance {
/// The current state of the VM.
pub vm_state: Mutex<VmState>,
+ /// Handle to global resources allocated for this VM.
+ #[allow(dead_code)] // The handle is never read, we only need to hold it.
+ vm_context: Strong<dyn IGlobalVmContext>,
/// The CID assigned to the VM for vsock communication.
pub cid: Cid,
/// The name of the VM.
@@ -190,12 +221,10 @@
pub requester_debug_pid: i32,
/// Callbacks to clients of the VM.
pub callbacks: VirtualMachineCallbacks,
- /// Input/output stream of the payload run in the VM.
- pub stream: Mutex<Option<VsockStream>>,
/// VirtualMachineService binder object for the VM.
pub vm_service: Mutex<Option<Strong<dyn IVirtualMachineService>>>,
- /// Recorded timestamp when the VM is started.
- pub vm_start_timestamp: Mutex<Option<SystemTime>>,
+ /// Recorded metrics of VM such as timestamp or cpu / memory usage.
+ pub vm_metric: Mutex<VmMetric>,
/// The latest lifecycle state which the payload reported itself to be in.
payload_state: Mutex<PayloadState>,
/// Represents the condition that payload_state was updated
@@ -209,6 +238,7 @@
temporary_directory: PathBuf,
requester_uid: u32,
requester_debug_pid: i32,
+ vm_context: Strong<dyn IGlobalVmContext>,
) -> Result<VmInstance, Error> {
validate_config(&config)?;
let cid = config.cid;
@@ -216,6 +246,7 @@
let protected = config.protected;
Ok(VmInstance {
vm_state: Mutex::new(VmState::NotStarted { config }),
+ vm_context,
cid,
name,
protected,
@@ -223,9 +254,8 @@
requester_uid,
requester_debug_pid,
callbacks: Default::default(),
- stream: Mutex::new(None),
vm_service: Mutex::new(None),
- vm_start_timestamp: Mutex::new(None),
+ vm_metric: Mutex::new(Default::default()),
payload_state: Mutex::new(PayloadState::Starting),
payload_state_updated: Condvar::new(),
})
@@ -234,7 +264,8 @@
/// Starts an instance of `crosvm` to manage the VM. The `crosvm` instance will be killed when
/// the `VmInstance` is dropped.
pub fn start(self: &Arc<Self>) -> Result<(), Error> {
- *self.vm_start_timestamp.lock().unwrap() = Some(SystemTime::now());
+ let mut vm_metric = self.vm_metric.lock().unwrap();
+ vm_metric.start_timestamp = Some(SystemTime::now());
self.vm_state.lock().unwrap().start(self.clone())
}
@@ -283,13 +314,8 @@
let death_reason = death_reason(&result, &failure_reason);
self.callbacks.callback_on_died(self.cid, death_reason);
- let vm_start_timestamp = self.vm_start_timestamp.lock().unwrap();
- write_vm_exited_stats(
- self.requester_uid as i32,
- &self.name,
- death_reason,
- *vm_start_timestamp,
- );
+ let vm_metric = self.vm_metric.lock().unwrap();
+ write_vm_exited_stats(self.requester_uid as i32, &self.name, death_reason, &*vm_metric);
// Delete temporary files.
if let Err(e) = remove_dir_all(&self.temporary_directory) {
@@ -321,6 +347,42 @@
}
}
+ fn monitor_vm_status(&self, child: Arc<SharedChild>) {
+ let pid = child.id();
+
+ loop {
+ {
+ // Check VM state
+ let vm_state = &*self.vm_state.lock().unwrap();
+ if let VmState::Dead = vm_state {
+ break;
+ }
+
+ let mut vm_metric = self.vm_metric.lock().unwrap();
+
+ // Get CPU Information
+ // TODO: Collect it once right before VM dies using SIGCHLD
+ if let Ok(guest_time) = get_guest_time(pid) {
+ vm_metric.cpu_guest_time = Some(guest_time);
+ } else {
+ error!("Failed to parse /proc/[pid]/stat");
+ }
+
+ // Get Memory Information
+ if let Ok(rss) = get_rss(pid) {
+ vm_metric.rss = match &vm_metric.rss {
+ Some(x) => Some(Rss::extract_max(x, &rss)),
+ None => Some(rss),
+ }
+ } else {
+ error!("Failed to parse /proc/[pid]/smaps");
+ }
+ }
+
+ thread::sleep(Duration::from_secs(1));
+ }
+ }
+
/// Returns the last reported state of the VM payload.
pub fn payload_state(&self) -> PayloadState {
*self.payload_state.lock().unwrap()
@@ -391,6 +453,60 @@
}
}
+impl Rss {
+ fn extract_max(x: &Rss, y: &Rss) -> Rss {
+ Rss { vm: max(x.vm, y.vm), crosvm: max(x.crosvm, y.crosvm) }
+ }
+}
+
+// Get guest time from /proc/[crosvm pid]/stat
+fn get_guest_time(pid: u32) -> Result<i64> {
+ let file = read_to_string(format!("/proc/{}/stat", pid))?;
+ let data_list: Vec<_> = file.split_whitespace().collect();
+
+ // Information about guest_time is at 43th place of the file split with the whitespace.
+ // Example of /proc/[pid]/stat :
+ // 6603 (kworker/104:1H-kblockd) I 2 0 0 0 -1 69238880 0 0 0 0 0 88 0 0 0 -20 1 0 1845 0 0
+ // 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 0 0 0 17 104 0 0 0 0 0 0 0 0 0 0 0 0 0
+ if data_list.len() < 43 {
+ bail!("Failed to parse command result for getting guest time : {}", file);
+ }
+
+ let guest_time_ticks = data_list[42].parse::<i64>()?;
+ // SAFETY : It just returns an integer about CPU tick information.
+ let ticks_per_sec = unsafe { sysconf(_SC_CLK_TCK) } as i64;
+ Ok(guest_time_ticks * MILLIS_PER_SEC / ticks_per_sec)
+}
+
+// Get rss from /proc/[crosvm pid]/smaps
+fn get_rss(pid: u32) -> Result<Rss> {
+ let file = read_to_string(format!("/proc/{}/smaps", pid))?;
+ let lines: Vec<_> = file.split('\n').collect();
+
+ let mut rss_vm_total = 0i64;
+ let mut rss_crosvm_total = 0i64;
+ let mut is_vm = false;
+ for line in lines {
+ if line.contains("crosvm_guest") {
+ is_vm = true;
+ } else if line.contains("Rss:") {
+ let data_list: Vec<_> = line.split_whitespace().collect();
+ if data_list.len() < 2 {
+ bail!("Failed to parse command result for getting rss :\n{}", line);
+ }
+ let rss = data_list[1].parse::<i64>()?;
+
+ if is_vm {
+ rss_vm_total += rss;
+ is_vm = false;
+ }
+ rss_crosvm_total += rss;
+ }
+ }
+
+ Ok(Rss { vm: rss_vm_total, crosvm: rss_crosvm_total })
+}
+
fn death_reason(result: &Result<ExitStatus, io::Error>, mut failure_reason: &str) -> DeathReason {
if let Some(position) = failure_reason.find('|') {
// Separator indicates extra context information is present after the failure name.
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 53402da..1f0433d 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -27,7 +27,7 @@
use binder::ParcelFileDescriptor;
use microdroid_payload_config::VmPayloadConfig;
use std::fs::File;
-use std::io::{self, BufRead, BufReader};
+use std::io;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::path::{Path, PathBuf};
use vmclient::{ErrorCode, VmInstance};
@@ -276,19 +276,8 @@
struct Callback {}
impl vmclient::VmCallback for Callback {
- fn on_payload_started(&self, _cid: i32, stream: Option<&File>) {
- // Show the output of the payload
- if let Some(stream) = stream {
- let mut reader = BufReader::new(stream.try_clone().unwrap());
- std::thread::spawn(move || loop {
- let mut s = String::new();
- match reader.read_line(&mut s) {
- Ok(0) => break,
- Ok(_) => print!("{}", s),
- Err(e) => eprintln!("error reading from virtual machine: {}", e),
- };
- });
- }
+ fn on_payload_started(&self, _cid: i32) {
+ eprintln!("payload started");
}
fn on_payload_ready(&self, _cid: i32) {
diff --git a/vmbase/example/Android.bp b/vmbase/example/Android.bp
index e9a3f98..0f1e66a 100644
--- a/vmbase/example/Android.bp
+++ b/vmbase/example/Android.bp
@@ -54,10 +54,11 @@
edition: "2021",
rustlibs: [
"android.system.virtualizationservice-rust",
+ "libandroid_logger",
"libanyhow",
- "libenv_logger",
"liblibc",
"liblog_rust",
+ "libnix",
"libvmclient",
],
data: [
diff --git a/vmbase/example/tests/test.rs b/vmbase/example/tests/test.rs
index 85e0213..57b68ed 100644
--- a/vmbase/example/tests/test.rs
+++ b/vmbase/example/tests/test.rs
@@ -25,8 +25,9 @@
use log::info;
use std::{
fs::File,
- io,
- os::unix::io::{AsRawFd, FromRawFd},
+ io::{self, BufRead, BufReader},
+ os::unix::io::FromRawFd,
+ panic, thread,
};
use vmclient::{DeathReason, VmInstance};
@@ -36,7 +37,14 @@
/// Runs the vmbase_example VM as an unprotected VM via VirtualizationService.
#[test]
fn test_run_example_vm() -> Result<(), Error> {
- env_logger::init();
+ android_logger::init_once(
+ android_logger::Config::default().with_tag("vmbase").with_min_level(log::Level::Debug),
+ );
+
+ // Redirect panic messages to logcat.
+ panic::set_hook(Box::new(|panic_info| {
+ log::error!("{}", panic_info);
+ }));
// We need to start the thread pool for Binder to work properly, especially link_to_death.
ProcessState::start_thread_pool();
@@ -62,8 +70,8 @@
platformVersion: "~1.0".to_string(),
taskProfiles: vec![],
});
- let console = duplicate_stdout()?;
- let log = duplicate_stdout()?;
+ let console = android_log_fd()?;
+ let log = android_log_fd()?;
let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log), None)
.context("Failed to create VM")?;
vm.start().context("Failed to start VM")?;
@@ -76,17 +84,17 @@
Ok(())
}
-/// Safely duplicate the standard output file descriptor.
-fn duplicate_stdout() -> io::Result<File> {
- let stdout_fd = io::stdout().as_raw_fd();
- // Safe because this just duplicates a file descriptor which we know to be valid, and we check
- // for an error.
- let dup_fd = unsafe { libc::dup(stdout_fd) };
- if dup_fd < 0 {
- Err(io::Error::last_os_error())
- } else {
- // Safe because we have just duplicated the file descriptor so we own it, and `from_raw_fd`
- // takes ownership of it.
- Ok(unsafe { File::from_raw_fd(dup_fd) })
- }
+fn android_log_fd() -> io::Result<File> {
+ let (reader_fd, writer_fd) = nix::unistd::pipe()?;
+
+ // SAFETY: These are new FDs with no previous owner.
+ let reader = unsafe { File::from_raw_fd(reader_fd) };
+ let writer = unsafe { File::from_raw_fd(writer_fd) };
+
+ thread::spawn(|| {
+ for line in BufReader::new(reader).lines() {
+ info!("{}", line.unwrap());
+ }
+ });
+ Ok(writer)
}
diff --git a/vmclient/src/lib.rs b/vmclient/src/lib.rs
index e6f32b4..20b7f02 100644
--- a/vmclient/src/lib.rs
+++ b/vmclient/src/lib.rs
@@ -74,7 +74,7 @@
pub trait VmCallback {
/// Called when the payload has been started within the VM. If present, `stream` is connected
/// to the stdin/stdout of the payload.
- fn on_payload_started(&self, cid: i32, stream: Option<&File>) {}
+ fn on_payload_started(&self, cid: i32) {}
/// Callend when the payload has notified Virtualization Service that it is ready to serve
/// clients.
@@ -269,14 +269,10 @@
impl Interface for VirtualMachineCallback {}
impl IVirtualMachineCallback for VirtualMachineCallback {
- fn onPayloadStarted(
- &self,
- cid: i32,
- stream: Option<&ParcelFileDescriptor>,
- ) -> BinderResult<()> {
+ fn onPayloadStarted(&self, cid: i32) -> BinderResult<()> {
self.state.notify_state(VirtualMachineState::STARTED);
if let Some(ref callback) = self.client_callback {
- callback.on_payload_started(cid, stream.map(ParcelFileDescriptor::as_ref));
+ callback.on_payload_started(cid);
}
Ok(())
}