Merge "pvmfw: README: Detail boot.img header's kernel_size"
diff --git a/authfs/Android.bp b/authfs/Android.bp
index 523da35..154a1d6 100644
--- a/authfs/Android.bp
+++ b/authfs/Android.bp
@@ -33,9 +33,6 @@
enabled: false,
},
},
- shared_libs: [
- "libbinder_rpc_unstable",
- ],
defaults: ["crosvm_defaults"],
}
diff --git a/authfs/fd_server/Android.bp b/authfs/fd_server/Android.bp
index f7cb5e3..5097408 100644
--- a/authfs/fd_server/Android.bp
+++ b/authfs/fd_server/Android.bp
@@ -18,9 +18,6 @@
"librpcbinder_rs",
],
prefer_rlib: true,
- shared_libs: [
- "libbinder_rpc_unstable",
- ],
apex_available: ["com.android.virt"],
}
@@ -40,8 +37,5 @@
"librpcbinder_rs",
],
prefer_rlib: true,
- shared_libs: [
- "libbinder_rpc_unstable",
- ],
test_suites: ["general-tests"],
}
diff --git a/authfs/src/file/dir.rs b/authfs/src/file/dir.rs
index f3cc6f8..5d2ec9f 100644
--- a/authfs/src/file/dir.rs
+++ b/authfs/src/file/dir.rs
@@ -28,7 +28,7 @@
use crate::fsverity::VerifiedFileEditor;
use crate::fusefs::{AuthFsDirEntry, Inode};
-const MAX_ENTRIES: u16 = 100; // Arbitrary limit
+const MAX_ENTRIES: u16 = 1000; // Arbitrary limit
struct InodeInfo {
inode: Inode,
diff --git a/authfs/tests/benchmarks/Android.bp b/authfs/tests/benchmarks/Android.bp
index 9bdef7b..38ece79 100644
--- a/authfs/tests/benchmarks/Android.bp
+++ b/authfs/tests/benchmarks/Android.bp
@@ -23,7 +23,6 @@
":authfs_test_files",
":CtsApkVerityTestPrebuiltFiles",
":MicrodroidTestApp",
- ":measure_io",
],
}
@@ -36,3 +35,19 @@
"libbase",
],
}
+
+// Package measure_io binary into a jar, to bundle with the MicrodroidTestApp.
+// When MicrodroidTestApp is mounted inside the Microdroid, the zipfuse will
+// add the +x permission on it.
+java_genrule {
+ name: "measure_io_as_jar",
+ out: ["measure_io.jar"],
+ srcs: [
+ ":measure_io",
+ ],
+ cmd: "out_dir=$$(dirname $(out))" +
+ "&& bin_dir=\"bin\" " +
+ "&& mkdir -p $$out_dir/$$bin_dir" +
+ "&& cp $(in) $$out_dir/$$bin_dir" +
+ "&& jar cf $(out) -C $$out_dir $$bin_dir",
+}
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 32eafb8..085d06e 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
@@ -18,7 +18,6 @@
import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
-import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
@@ -45,7 +44,6 @@
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;
-import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -57,11 +55,8 @@
public class AuthFsBenchmarks extends BaseHostJUnit4Test {
private static final int TRIAL_COUNT = 5;
- /** Name of the measure_io binary on host. */
- private static final String MEASURE_IO_BIN_NAME = "measure_io";
-
/** Path to measure_io on Microdroid. */
- private static final String MEASURE_IO_BIN_PATH = "/data/local/tmp/measure_io";
+ private static final String MEASURE_IO_BIN_PATH = "/mnt/apk/bin/measure_io";
/** fs-verity digest (sha256) of testdata/input.4m */
private static final String DIGEST_4M =
@@ -123,7 +118,6 @@
}
private void readRemoteFile(String mode) throws DeviceNotAvailableException {
- pushMeasureIoBinToMicrodroid();
// Cache the file in memory for the host.
mAuthFsTestRule
.getAndroid()
@@ -146,7 +140,6 @@
}
private void writeRemoteFile(String mode) throws DeviceNotAvailableException {
- pushMeasureIoBinToMicrodroid();
String filePath = mAuthFsTestRule.MOUNT_DIR + "/5";
int fileSizeMb = 8;
String cmd = MEASURE_IO_BIN_PATH + " " + filePath + " " + fileSizeMb + " " + mode + " w";
@@ -165,14 +158,6 @@
reportMetrics(rates, mode + "_write", "mb_per_sec");
}
- private void pushMeasureIoBinToMicrodroid() throws DeviceNotAvailableException {
- File measureReadBin = mAuthFsTestRule.findTestFile(getBuild(), MEASURE_IO_BIN_NAME);
- assertThat(measureReadBin.exists()).isTrue();
- mAuthFsTestRule.getMicrodroidDevice().pushFile(measureReadBin, MEASURE_IO_BIN_PATH);
- assertThat(mAuthFsTestRule.getMicrodroid().run("ls " + MEASURE_IO_BIN_PATH))
- .isEqualTo(MEASURE_IO_BIN_PATH);
- }
-
private void reportMetrics(List<Double> metrics, String name, String unit) {
Map<String, Double> stats = mMetricsProcessor.computeStats(metrics, name, unit);
for (Map.Entry<String, Double> entry : stats.entrySet()) {
diff --git a/compos/Android.bp b/compos/Android.bp
index 0890e9d..2f6be98 100644
--- a/compos/Android.bp
+++ b/compos/Android.bp
@@ -27,7 +27,6 @@
],
prefer_rlib: true,
shared_libs: [
- "libbinder_rpc_unstable",
"libcrypto",
],
}
diff --git a/compos/aidl/com/android/compos/ICompOsService.aidl b/compos/aidl/com/android/compos/ICompOsService.aidl
index df8c91e..497c35e 100644
--- a/compos/aidl/com/android/compos/ICompOsService.aidl
+++ b/compos/aidl/com/android/compos/ICompOsService.aidl
@@ -87,7 +87,7 @@
/**
* Returns the attestation certificate chain of the current VM. The result is in the form of a
* CBOR encoded Boot Certificate Chain (BCC) as defined in
- * hardware/interfaces/security/dice/aidl/android/hardware/security/dice/Bcc.aidl.
+ * hardware/interfaces/security/rkp/aidl/android/hardware/security/keymint/ProtectedData.aidl
*/
byte[] getAttestationChain();
diff --git a/compos/common/Android.bp b/compos/common/Android.bp
index 7a7042e..05bc093 100644
--- a/compos/common/Android.bp
+++ b/compos/common/Android.bp
@@ -12,6 +12,7 @@
"compos_aidl_interface-rust",
"libanyhow",
"libbinder_rs",
+ "libglob",
"liblazy_static",
"liblog_rust",
"libnested_virt",
@@ -20,9 +21,6 @@
"libvmclient",
],
proc_macros: ["libnum_derive"],
- shared_libs: [
- "libbinder_rpc_unstable",
- ],
apex_available: [
"com.android.compos",
],
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 92c9a3c..96c8147 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -27,12 +27,13 @@
VirtualMachineAppConfig::{DebugLevel::DebugLevel, Payload::Payload, VirtualMachineAppConfig},
VirtualMachineConfig::VirtualMachineConfig,
};
-use anyhow::{bail, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
use binder::{ParcelFileDescriptor, Strong};
use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
+use glob::glob;
use log::{info, warn};
use rustutils::system_properties;
-use std::fs::{self, File};
+use std::fs::File;
use std::path::{Path, PathBuf};
use vmclient::{DeathReason, ErrorCode, VmInstance, VmWaitError};
@@ -194,15 +195,19 @@
// Our config APK will be in a directory under app, but the name of the directory is at the
// discretion of the build system. So just look in each sub-directory until we find it.
// (In practice there will be exactly one directory, so this shouldn't take long.)
- let app_dir = apex_dir.join("app");
- for dir in fs::read_dir(app_dir).context("Reading app dir")? {
- let apk_file = dir?.path().join("CompOSPayloadApp.apk");
- if apk_file.is_file() {
- return Ok(apk_file);
- }
+ let app_glob = apex_dir.join("app").join("**").join("CompOSPayloadApp*.apk");
+ let mut entries: Vec<PathBuf> =
+ glob(app_glob.to_str().ok_or_else(|| anyhow!("Invalid path: {}", app_glob.display()))?)
+ .context("failed to glob")?
+ .filter_map(|e| e.ok())
+ .collect();
+ if entries.len() > 1 {
+ bail!("Found more than one apk matching {}", app_glob.display());
}
-
- bail!("Failed to locate CompOSPayloadApp.apk")
+ match entries.pop() {
+ Some(path) => Ok(path),
+ None => Err(anyhow!("No apks match {}", app_glob.display())),
+ }
}
fn prepare_idsig(
diff --git a/docs/debug/ramdump.md b/docs/debug/ramdump.md
index 771c608..020f054 100644
--- a/docs/debug/ramdump.md
+++ b/docs/debug/ramdump.md
@@ -73,8 +73,8 @@
Download the source code and build it as follows. This needs to be done only once.
```shell
-$ wget https://github.com/crash-utility/crash/archive/refs/tags/8.0.1.tar.gz -O - | tar xzvf
-$ make -C crash-8.0.1 target=ARM64
+$ wget https://github.com/crash-utility/crash/archive/refs/tags/8.0.2.tar.gz -O - | tar xzv
+$ make -j -C crash-8.0.2 target=ARM64
```
### Obtaining vmlinux
@@ -101,7 +101,7 @@
### Running crash(8) with the RAM dump and the kernel image
```shell
-$ crash-8.0.1/crash ramdump vmlinux
+$ crash-8.0.2/crash ramdump vmlinux
```
You can now analyze the RAM dump using the various commands that crash(8) provides. For example, `bt <pid>` command shows the stack trace of a process.
diff --git a/docs/debug/tracing.md b/docs/debug/tracing.md
new file mode 100644
index 0000000..7d7ea0c
--- /dev/null
+++ b/docs/debug/tracing.md
@@ -0,0 +1,75 @@
+# Hypervisor & guest tracing
+
+## Hypervisor tracing
+
+Starting with android14-5.15 kernel it is possible to get traces from the hypervisor.
+
+### User space interface
+
+The user space hypervisor tracing interface is located either at /sys/kernel/tracing/hyp or at
+/sys/kernel/debug/tracing/hyp. On the Android phones it will usually be /sys/kernel/tracing/hyp,
+while on QEMU it will be /sys/kernel/debug/tracing/hyp.
+
+The user space interface is very similar to the ftrace user space interface, however there are some
+differences, e.g.:
+
+* Only boot clock is supported, and there is no way for user space to change the tracing_clock.
+* Hypervisor tracing periodically polls the data from the hypervisor, this is different from the
+ regular ftrace instance which pushes the events into the ring buffer.
+
+Note: the list above is not exhaustive.
+
+TODO(b/271412868): add more documentation on the user space interface.
+
+### Perfetto integration
+
+[Perfetto](https://perfetto.dev/docs/) is an open-source stack for performance instrumentation and
+trace analysis widely used in Android. Perfetto supports capturing and visualizing hypervisor
+traces.
+
+#### Capturing hypervisor traces on Android
+
+Consider first familiarizing yourself with Perfetto documentation for recording traces on Android:
+https://perfetto.dev/docs/quickstart/android-tracing.
+
+So far it is only possible to capture hypervisor trace events by providing the full trace config
+file to Perfetto. Here is the minimal
+
+```shell
+cat<<EOF>config.pbtx
+duration_ms: 10000
+
+buffers: {
+ size_kb: 8960
+ fill_policy: DISCARD
+}
+
+data_sources: {
+ config {
+ name: "linux.ftrace"
+ ftrace_config {
+ instance_name: "hyp"
+ ftrace_events: "hyp/hyp_enter"
+ ftrace_events: "hyp/hyp_exit"
+ }
+ }
+}
+EOF
+
+./record_android_trace -c config.pbtx -o trace_file.perfetto-trace
+```
+
+If you have an Android tree checked out, then record_android_trace helper script can be located at
+${REPO_ROOT}/external/perfetto/tools/record_android_traces. Otherwise, you can download the script
+by following steps outlined in the [Perfetto docs](
+https://perfetto.dev/docs/quickstart/android-tracing#recording-a-trace-through-the-cmdline)
+
+#### Capturing hypervisor traces on QEMU
+
+TODO(b/271412868): fill in this section
+
+TODO(b/271412868): Stay tuned, more docs coming soon!
+
+## Microdroid VM tracing
+
+TODO(b/271412868): Stay tuned, more docs are coming soon!
diff --git a/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp b/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp
index bd80880..6659a57 100644
--- a/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp
+++ b/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp
@@ -83,7 +83,7 @@
ARpcSession_setFileDescriptorTransportMode(session.get(),
ARpcSession_FileDescriptorTransportMode::Unix);
ARpcSession_setMaxIncomingThreads(session.get(), VIRTMGR_THREADS);
- ARpcSession_setMaxOutgoingThreads(session.get(), VIRTMGR_THREADS);
+ ARpcSession_setMaxOutgoingConnections(session.get(), VIRTMGR_THREADS);
// SAFETY - ARpcSession_setupUnixDomainBootstrapClient does not take ownership of clientFd.
auto client = ARpcSession_setupUnixDomainBootstrapClient(session.get(), clientFd);
return AIBinder_toJavaBinder(env, client);
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index a6b3ed6..5f39b1c 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -1033,11 +1033,13 @@
}
checkStopped();
- // Delete any existing file before recreating; that ensures any VirtualMachineDescriptor
- // that refers to the old file does not see the new config.
- mConfigFilePath.delete();
- newConfig.serialize(mConfigFilePath);
- mConfig = newConfig;
+ if (oldConfig != newConfig) {
+ // Delete any existing file before recreating; that ensures any
+ // VirtualMachineDescriptor that refers to the old file does not see the new config.
+ mConfigFilePath.delete();
+ newConfig.serialize(mConfigFilePath);
+ mConfig = newConfig;
+ }
return oldConfig;
}
}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index c364b42..93e65db 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -31,11 +31,13 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.sysprop.HypervisorProperties;
import android.system.virtualizationservice.VirtualMachineAppConfig;
import android.system.virtualizationservice.VirtualMachinePayloadConfig;
+import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
@@ -47,6 +49,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
+import java.util.zip.ZipFile;
/**
* Represents a configuration of a virtual machine. A configuration consists of hardware
@@ -57,6 +60,7 @@
*/
@SystemApi
public final class VirtualMachineConfig {
+ private static final String TAG = "VirtualMachineConfig";
private static final String[] EMPTY_STRING_ARRAY = {};
// These define the schema of the config file persisted on disk.
@@ -296,8 +300,7 @@
/**
* Returns the absolute path of the APK which should contain the binary payload that will
- * execute within the VM. Returns null if no specific path has been set, so the primary APK will
- * be used.
+ * execute within the VM. Returns null if no specific path has been set.
*
* @hide
*/
@@ -421,6 +424,9 @@
*/
@SystemApi
public boolean isCompatibleWith(@NonNull VirtualMachineConfig other) {
+ if (this == other) {
+ return true;
+ }
return this.mDebugLevel == other.mDebugLevel
&& this.mProtectedVm == other.mProtectedVm
&& this.mEncryptedStorageBytes == other.mEncryptedStorageBytes
@@ -442,18 +448,7 @@
throws VirtualMachineException {
VirtualMachineAppConfig vsConfig = new VirtualMachineAppConfig();
- String apkPath = mApkPath;
- if (apkPath == null) {
- try {
- ApplicationInfo appInfo =
- packageManager.getApplicationInfo(
- mPackageName, PackageManager.ApplicationInfoFlags.of(0));
- // This really is the path to the APK, not a directory.
- apkPath = appInfo.sourceDir;
- } catch (PackageManager.NameNotFoundException e) {
- throw new VirtualMachineException("Package not found", e);
- }
- }
+ String apkPath = (mApkPath != null) ? mApkPath : findPayloadApk(packageManager);
try {
vsConfig.apk = ParcelFileDescriptor.open(new File(apkPath), MODE_READ_ONLY);
@@ -492,6 +487,45 @@
return vsConfig;
}
+ private String findPayloadApk(PackageManager packageManager) throws VirtualMachineException {
+ ApplicationInfo appInfo;
+ try {
+ appInfo =
+ packageManager.getApplicationInfo(
+ mPackageName, PackageManager.ApplicationInfoFlags.of(0));
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new VirtualMachineException("Package not found", e);
+ }
+
+ String[] splitApkPaths = appInfo.splitSourceDirs;
+ String[] abis = Build.SUPPORTED_64_BIT_ABIS;
+
+ // If there are split APKs, and we know the payload binary name, see if we can find a
+ // split APK containing the binary.
+ if (mPayloadBinaryName != null && splitApkPaths != null && abis.length != 0) {
+ String[] libraryNames = new String[abis.length];
+ for (int i = 0; i < abis.length; i++) {
+ libraryNames[i] = "lib/" + abis[i] + "/" + mPayloadBinaryName;
+ }
+
+ for (String path : splitApkPaths) {
+ try (ZipFile zip = new ZipFile(path)) {
+ for (String name : libraryNames) {
+ if (zip.getEntry(name) != null) {
+ Log.i(TAG, "Found payload in " + path);
+ return path;
+ }
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to scan split APK: " + path, e);
+ }
+ }
+ }
+
+ // This really is the path to the APK, not a directory.
+ return appInfo.sourceDir;
+ }
+
private int bytesToMebiBytes(long mMemoryBytes) {
long oneMebi = 1024 * 1024;
// We can't express requests for more than 2 exabytes, but then they're not going to succeed
@@ -593,7 +627,8 @@
/**
* Sets the absolute path of the APK containing the binary payload that will execute within
- * the VM. If not set explicitly, defaults to the primary APK of the context.
+ * the VM. If not set explicitly, defaults to the split APK containing the payload, if there
+ * is one, and otherwise the primary APK of the context.
*
* @hide
*/
diff --git a/libs/apkverify/src/algorithms.rs b/libs/apkverify/src/algorithms.rs
index 442b47c..c05ab38 100644
--- a/libs/apkverify/src/algorithms.rs
+++ b/libs/apkverify/src/algorithms.rs
@@ -204,9 +204,10 @@
}
/// Hash algorithms.
-#[derive(Clone, Copy, Debug, PartialEq, Eq, FromPrimitive, ToPrimitive)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, FromPrimitive, ToPrimitive, Default)]
#[repr(u32)]
pub enum HashAlgorithm {
+ #[default]
/// SHA-256
SHA256 = 1,
}
@@ -217,9 +218,3 @@
Self::from_u32(val).context(format!("Unsupported hash algorithm: {}", val))
}
}
-
-impl Default for HashAlgorithm {
- fn default() -> Self {
- HashAlgorithm::SHA256
- }
-}
diff --git a/libs/apkverify/src/v4.rs b/libs/apkverify/src/v4.rs
index 94abf99..e77ad77 100644
--- a/libs/apkverify/src/v4.rs
+++ b/libs/apkverify/src/v4.rs
@@ -104,9 +104,10 @@
}
/// Version of the idsig file format
-#[derive(Debug, PartialEq, Eq, FromPrimitive, ToPrimitive)]
+#[derive(Debug, PartialEq, Eq, FromPrimitive, ToPrimitive, Default)]
#[repr(u32)]
pub enum Version {
+ #[default]
/// Version 2, the only supported version.
V2 = 2,
}
@@ -117,12 +118,6 @@
}
}
-impl Default for Version {
- fn default() -> Self {
- Version::V2
- }
-}
-
impl V4Signature<fs::File> {
/// Creates a `V4Signature` struct from the given idsig path.
pub fn from_idsig_path<P: AsRef<Path>>(idsig_path: P) -> Result<Self> {
diff --git a/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index 927bf50..1d295eb 100644
--- a/libs/libfdt/src/lib.rs
+++ b/libs/libfdt/src/lib.rs
@@ -405,6 +405,24 @@
fdt_err_expect_zero(ret)
}
+ /// Create or change a flag-like empty property.
+ pub fn setprop_empty(&mut self, name: &CStr) -> Result<()> {
+ self.setprop(name, &[])
+ }
+
+ /// Delete the given property.
+ pub fn delprop(&mut self, name: &CStr) -> Result<()> {
+ // SAFETY - Accesses are constrained to the DT totalsize (validated by ctor) when the
+ // library locates the node's property. Removing the property may shift the offsets of
+ // other nodes and properties but the borrow checker should prevent this function from
+ // being called when FdtNode instances are in use.
+ let ret = unsafe {
+ libfdt_bindgen::fdt_delprop(self.fdt.as_mut_ptr(), self.offset, name.as_ptr())
+ };
+
+ fdt_err_expect_zero(ret)
+ }
+
/// Get reference to the containing device tree.
pub fn fdt(&mut self) -> &mut Fdt {
self.fdt
@@ -561,6 +579,11 @@
self.node(CStr::from_bytes_with_nul(b"/chosen\0").unwrap())
}
+ /// Retrieve the standard /chosen node as mutable.
+ pub fn chosen_mut(&mut self) -> Result<Option<FdtNodeMut>> {
+ self.node_mut(CStr::from_bytes_with_nul(b"/chosen\0").unwrap())
+ }
+
/// Get the root node of the tree.
pub fn root(&self) -> Result<FdtNode> {
self.node(CStr::from_bytes_with_nul(b"/\0").unwrap())?.ok_or(FdtError::Internal)
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index dc59fff..bc4db6c 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -85,13 +85,6 @@
"microdroid_property_contexts",
"mke2fs.microdroid",
- // Adding more libs manually for unbundled build.
- // TODO(b/268557568) these should be added automatically.
- "libcrypto",
- "liblzma",
- "libc++",
- "libssl",
-
"libvm_payload", // used by payload to interact with microdroid manager
"prng_seeder_microdroid",
diff --git a/microdroid/init.rc b/microdroid/init.rc
index ce0cab4..70c22d4 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -98,7 +98,7 @@
mount rootfs rootfs / remount bind ro nodev
# TODO(b/185767624): change the hard-coded size?
- mount tmpfs tmpfs /data noatime nosuid nodev rw size=128M
+ mount tmpfs tmpfs /data noatime nosuid nodev noexec rw size=128M
# We chown/chmod /data again so because mount is run as root + defaults
chown system system /data
diff --git a/microdroid/kdump/kernel/arm64/kernel-5.15 b/microdroid/kdump/kernel/arm64/kernel-5.15
index 0f2172b..28b0214 100644
--- a/microdroid/kdump/kernel/arm64/kernel-5.15
+++ b/microdroid/kdump/kernel/arm64/kernel-5.15
Binary files differ
diff --git a/microdroid/payload/Android.bp b/microdroid/payload/Android.bp
index c8d1044..4814a64 100644
--- a/microdroid/payload/Android.bp
+++ b/microdroid/payload/Android.bp
@@ -46,28 +46,3 @@
type: "lite",
},
}
-
-cc_binary_host {
- name: "mk_payload",
- srcs: [
- "mk_payload.cc",
- ],
- static_libs: [
- "lib_microdroid_metadata_proto",
- "libbase",
- "libcdisk_spec",
- "libcuttlefish_fs",
- "libcuttlefish_host_config",
- "libcuttlefish_utils",
- "libext2_uuid",
- "libimage_aggregator",
- "libjsoncpp",
- "liblog",
- "libprotobuf-cpp-full",
- "libprotobuf-cpp-lite",
- "libsparse",
- "libxml2",
- "libz",
- ],
- static_executable: true,
-}
diff --git a/microdroid/payload/README.md b/microdroid/payload/README.md
index c2f624a..b1eb63f 100644
--- a/microdroid/payload/README.md
+++ b/microdroid/payload/README.md
@@ -38,34 +38,3 @@
Each payload partition presents APEX or APK passed from the host.
The size of a payload partition must be a multiple of 4096 bytes.
-
-# `mk_payload`
-
-`mk_payload` is a small utility to create a payload disk image. It is used by ARCVM.
-
-```
-$ cat payload_config.json
-{
- "apexes": [
- {
- "name": "com.my.hello",
- "path": "hello.apex",
- }
- ],
- "apk": {
- "name": "com.my.world",
- "path": "/path/to/world.apk",
- "idsigPath": "/path/to/world.apk.idsig",
- }
-}
-$ m mk_payload
-$ mk_payload payload_config.json payload.img
-$ ls
-payload.img
-payload-footer.img
-payload-header.img
-payload-metadata.img
-payload-filler-0.img
-payload-filler-1.img
-...
-```
diff --git a/microdroid/payload/config/src/lib.rs b/microdroid/payload/config/src/lib.rs
index 925a543..cdef3e4 100644
--- a/microdroid/payload/config/src/lib.rs
+++ b/microdroid/payload/config/src/lib.rs
@@ -64,10 +64,11 @@
/// Payload's task can be one of plain executable
/// or an .so library which can be started via /system/bin/microdroid_launcher
-#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)]
pub enum TaskType {
/// Task's command indicates the path to the executable binary.
#[serde(rename = "executable")]
+ #[default]
Executable,
/// Task's command indicates the .so library in /mnt/apk/lib/{arch}
#[serde(rename = "microdroid_launcher")]
@@ -87,12 +88,6 @@
pub command: String,
}
-impl Default for TaskType {
- fn default() -> TaskType {
- TaskType::Executable
- }
-}
-
/// APEX config
/// For now, we only pass the name of APEX.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index bf2d755..495d3bb 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -19,9 +19,9 @@
"libbinder_rs",
"libbyteorder",
"libcap_rust",
+ "libciborium",
"libdiced_open_dice",
"libdiced_sample_inputs",
- "libdiced_utils",
"libglob",
"libhex",
"libitertools",
@@ -46,9 +46,6 @@
"libvsock",
"librand",
],
- shared_libs: [
- "libbinder_rpc_unstable",
- ],
init_rc: ["microdroid_manager.rc"],
multilib: {
lib32: {
diff --git a/microdroid_manager/src/dice.rs b/microdroid_manager/src/dice.rs
index c3136e8..3a2a1e6 100644
--- a/microdroid_manager/src/dice.rs
+++ b/microdroid_manager/src/dice.rs
@@ -16,12 +16,14 @@
use anyhow::{anyhow, bail, Context, Error, Result};
use byteorder::{NativeEndian, ReadBytesExt};
+use ciborium::{cbor, ser};
use diced_open_dice::{
bcc_handover_parse, retry_bcc_main_flow, BccHandover, Config, DiceArtifacts, DiceMode, Hash,
Hidden, InputValues, OwnedDiceArtifacts,
};
use keystore2_crypto::ZVec;
use libc::{c_void, mmap, munmap, MAP_FAILED, MAP_PRIVATE, PROT_READ};
+use microdroid_metadata::PayloadMetadata;
use openssl::hkdf::hkdf;
use openssl::md::Md;
use std::fs;
@@ -157,3 +159,70 @@
}
}
}
+
+/// Returns a configuration descriptor of the given payload following the BCC's specification:
+/// https://cs.android.com/android/platform/superproject/+/master:hardware/interfaces/security/rkp/aidl/android/hardware/security/keymint/ProtectedData.aidl
+/// {
+/// -70002: "Microdroid payload",
+/// ? -71000: tstr // payload_config_path
+/// ? -71001: PayloadConfig
+/// }
+/// PayloadConfig = {
+/// 1: tstr // payload_binary_name
+/// }
+pub fn format_payload_config_descriptor(payload_metadata: &PayloadMetadata) -> Result<Vec<u8>> {
+ const MICRODROID_PAYLOAD_COMPONENT_NAME: &str = "Microdroid payload";
+
+ let config_descriptor_cbor_value = match payload_metadata {
+ PayloadMetadata::config_path(payload_config_path) => cbor!({
+ -70002 => MICRODROID_PAYLOAD_COMPONENT_NAME,
+ -71000 => payload_config_path
+ }),
+ PayloadMetadata::config(payload_config) => cbor!({
+ -70002 => MICRODROID_PAYLOAD_COMPONENT_NAME,
+ -71001 => {1 => payload_config.payload_binary_name}
+ }),
+ }
+ .context("Failed to build a CBOR Value from payload metadata")?;
+ let mut config_descriptor = Vec::new();
+ ser::into_writer(&config_descriptor_cbor_value, &mut config_descriptor)?;
+ Ok(config_descriptor)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use microdroid_metadata::PayloadConfig;
+
+ #[test]
+ fn payload_metadata_with_path_formats_correctly() -> Result<()> {
+ let payload_metadata = PayloadMetadata::config_path("/config_path".to_string());
+ let config_descriptor = format_payload_config_descriptor(&payload_metadata)?;
+ static EXPECTED_CONFIG_DESCRIPTOR: &[u8] = &[
+ 0xa2, 0x3a, 0x00, 0x01, 0x11, 0x71, 0x72, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x64, 0x72,
+ 0x6f, 0x69, 0x64, 0x20, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x3a, 0x00, 0x01,
+ 0x15, 0x57, 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x70, 0x61, 0x74,
+ 0x68,
+ ];
+ assert_eq!(EXPECTED_CONFIG_DESCRIPTOR, &config_descriptor);
+ Ok(())
+ }
+
+ #[test]
+ fn payload_metadata_with_config_formats_correctly() -> Result<()> {
+ let payload_config = PayloadConfig {
+ payload_binary_name: "payload_binary".to_string(),
+ ..Default::default()
+ };
+ let payload_metadata = PayloadMetadata::config(payload_config);
+ let config_descriptor = format_payload_config_descriptor(&payload_metadata)?;
+ static EXPECTED_CONFIG_DESCRIPTOR: &[u8] = &[
+ 0xa2, 0x3a, 0x00, 0x01, 0x11, 0x71, 0x72, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x64, 0x72,
+ 0x6f, 0x69, 0x64, 0x20, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x3a, 0x00, 0x01,
+ 0x15, 0x58, 0xa1, 0x01, 0x6e, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x62,
+ 0x69, 0x6e, 0x61, 0x72, 0x79,
+ ];
+ assert_eq!(EXPECTED_CONFIG_DESCRIPTOR, &config_descriptor);
+ Ok(())
+ }
+}
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 1148c31..f83753c 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -21,7 +21,7 @@
mod swap;
mod vm_payload_service;
-use crate::dice::{DiceDriver, derive_sealing_key};
+use crate::dice::{DiceDriver, derive_sealing_key, format_payload_config_descriptor};
use crate::instance::{ApexData, ApkData, InstanceDisk, MicrodroidData, RootHash};
use crate::vm_payload_service::register_vm_payload_service;
use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::ErrorCode::ErrorCode;
@@ -35,7 +35,6 @@
use apkverify::{get_public_key_der, verify, V4Signature};
use binder::Strong;
use diced_open_dice::OwnedDiceArtifacts;
-use diced_utils::cbor::{encode_header, encode_number};
use glob::glob;
use itertools::sorted;
use libc::VMADDR_CID_HOST;
@@ -287,54 +286,14 @@
let code_hash = code_hash_ctx.finish();
let authority_hash = authority_hash_ctx.finish();
- // {
- // -70002: "Microdroid payload",
- // ? -71000: tstr // payload_config_path
- // ? -71001: PayloadConfig
- // }
- // PayloadConfig = {
- // 1: tstr // payload_binary_name
- // }
-
- let mut config_desc = vec![
- 0xa2, // map(2)
- 0x3a, 0x00, 0x01, 0x11, 0x71, // -70002
- 0x72, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x20, 0x70, 0x61, 0x79,
- 0x6c, 0x6f, 0x61, 0x64, // "Microdroid payload"
- ];
-
- match payload_metadata {
- PayloadMetadata::config_path(payload_config_path) => {
- encode_negative_number(-71000, &mut config_desc)?;
- encode_tstr(payload_config_path, &mut config_desc)?;
- }
- PayloadMetadata::config(payload_config) => {
- encode_negative_number(-71001, &mut config_desc)?;
- encode_header(5, 1, &mut config_desc)?; // map(1)
- encode_number(1, &mut config_desc)?;
- encode_tstr(&payload_config.payload_binary_name, &mut config_desc)?;
- }
- }
+ let config_descriptor = format_payload_config_descriptor(payload_metadata)?;
// Check debuggability, conservatively assuming it is debuggable
let debuggable = system_properties::read_bool(DEBUGGABLE_PROP, true)?;
// Send the details to diced
let hidden = verified_data.salt.clone().try_into().unwrap();
- dice.derive(code_hash, &config_desc, authority_hash, debuggable, hidden)
-}
-
-fn encode_tstr(tstr: &str, buffer: &mut Vec<u8>) -> Result<()> {
- let bytes = tstr.as_bytes();
- encode_header(3, bytes.len().try_into().unwrap(), buffer)?;
- buffer.extend_from_slice(bytes);
- Ok(())
-}
-
-fn encode_negative_number(n: i64, buffer: &mut dyn Write) -> Result<()> {
- ensure!(n < 0);
- let n = -1 - n;
- encode_header(1, n.try_into().unwrap(), buffer)
+ dice.derive(code_hash, &config_descriptor, authority_hash, debuggable, hidden)
}
fn is_strict_boot() -> bool {
@@ -721,6 +680,9 @@
// Use the salt from a verified instance, or generate a salt for a new instance.
let salt = if let Some(saved_data) = saved_data {
saved_data.salt.clone()
+ } else if is_strict_boot() {
+ // No need to add more entropy as a previous stage must have used a new, random salt.
+ vec![0u8; 64]
} else {
let mut salt = vec![0u8; 64];
salt.as_mut_slice().try_fill(&mut rand::thread_rng())?;
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 7561800..d78f4f2 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -13,6 +13,7 @@
],
rustlibs: [
"libaarch64_paging",
+ "libbssl_ffi_nostd",
"libbuddy_system_allocator",
"libdiced_open_dice_nostd",
"libfdtpci",
@@ -21,7 +22,9 @@
"libonce_cell_nostd",
"libpvmfw_avb_nostd",
"libpvmfw_embedded_key",
+ "libstatic_assertions",
"libtinyvec_nostd",
+ "libuuid_nostd",
"libvirtio_drivers",
"libvmbase",
"libzeroize_nostd",
diff --git a/pvmfw/src/config.rs b/pvmfw/src/config.rs
index f209784..f62a580 100644
--- a/pvmfw/src/config.rs
+++ b/pvmfw/src/config.rs
@@ -216,12 +216,22 @@
}
/// Get slice containing the platform BCC.
- pub fn get_bcc_mut(&mut self) -> &mut [u8] {
- &mut self.body[self.bcc_range.clone()]
- }
+ pub fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>) {
+ let bcc_start = self.bcc_range.start;
+ let bcc_end = self.bcc_range.len();
+ let (_, rest) = self.body.split_at_mut(bcc_start);
+ let (bcc, rest) = rest.split_at_mut(bcc_end);
- /// Get slice containing the platform debug policy.
- pub fn get_debug_policy(&mut self) -> Option<&mut [u8]> {
- self.dp_range.as_ref().map(|r| &mut self.body[r.clone()])
+ let dp = if let Some(dp_range) = &self.dp_range {
+ let dp_start = dp_range.start.checked_sub(self.bcc_range.end).unwrap();
+ let dp_end = dp_range.len();
+ let (_, rest) = rest.split_at_mut(dp_start);
+ let (dp, _) = rest.split_at_mut(dp_end);
+ Some(dp)
+ } else {
+ None
+ };
+
+ (bcc, dp)
}
}
diff --git a/pvmfw/src/crypto.rs b/pvmfw/src/crypto.rs
new file mode 100644
index 0000000..85dc6c9
--- /dev/null
+++ b/pvmfw/src/crypto.rs
@@ -0,0 +1,302 @@
+// Copyright 2023, 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.
+
+//! Wrapper around BoringSSL/OpenSSL symbols.
+
+use core::convert::AsRef;
+use core::ffi::{c_char, c_int, CStr};
+use core::fmt;
+use core::mem::MaybeUninit;
+use core::num::NonZeroU32;
+use core::ptr;
+
+use bssl_ffi::ERR_get_error_line;
+use bssl_ffi::ERR_lib_error_string;
+use bssl_ffi::ERR_reason_error_string;
+use bssl_ffi::EVP_AEAD_CTX_aead;
+use bssl_ffi::EVP_AEAD_CTX_init;
+use bssl_ffi::EVP_AEAD_CTX_open;
+use bssl_ffi::EVP_AEAD_CTX_seal;
+use bssl_ffi::EVP_AEAD_max_overhead;
+use bssl_ffi::EVP_aead_aes_256_gcm_randnonce;
+use bssl_ffi::EVP_sha512;
+use bssl_ffi::EVP_AEAD;
+use bssl_ffi::EVP_AEAD_CTX;
+use bssl_ffi::HKDF;
+
+#[derive(Debug)]
+pub struct Error {
+ packed: NonZeroU32,
+ file: Option<&'static CStr>,
+ line: c_int,
+}
+
+impl Error {
+ fn get() -> Option<Self> {
+ let mut file = MaybeUninit::uninit();
+ let mut line = MaybeUninit::uninit();
+ // SAFETY - The function writes to the provided pointers, validated below.
+ let packed = unsafe { ERR_get_error_line(file.as_mut_ptr(), line.as_mut_ptr()) };
+ // SAFETY - Any possible value returned could be considered a valid *const c_char.
+ let file = unsafe { file.assume_init() };
+ // SAFETY - Any possible value returned could be considered a valid c_int.
+ let line = unsafe { line.assume_init() };
+
+ let packed = packed.try_into().ok()?;
+ // SAFETY - Any non-NULL result is expected to point to a global const C string.
+ let file = unsafe { as_static_cstr(file) };
+
+ Some(Self { packed, file, line })
+ }
+
+ fn packed_value(&self) -> u32 {
+ self.packed.get()
+ }
+
+ fn library_name(&self) -> Option<&'static CStr> {
+ // SAFETY - Call to a pure function.
+ let name = unsafe { ERR_lib_error_string(self.packed_value()) };
+ // SAFETY - Any non-NULL result is expected to point to a global const C string.
+ unsafe { as_static_cstr(name) }
+ }
+
+ fn reason(&self) -> Option<&'static CStr> {
+ // SAFETY - Call to a pure function.
+ let reason = unsafe { ERR_reason_error_string(self.packed_value()) };
+ // SAFETY - Any non-NULL result is expected to point to a global const C string.
+ unsafe { as_static_cstr(reason) }
+ }
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let unknown_library = CStr::from_bytes_with_nul(b"{unknown library}\0").unwrap();
+ let unknown_reason = CStr::from_bytes_with_nul(b"{unknown reason}\0").unwrap();
+ let unknown_file = CStr::from_bytes_with_nul(b"??\0").unwrap();
+
+ let packed = self.packed_value();
+ let library = self.library_name().unwrap_or(unknown_library).to_str().unwrap();
+ let reason = self.reason().unwrap_or(unknown_reason).to_str().unwrap();
+ let file = self.file.unwrap_or(unknown_file).to_str().unwrap();
+ let line = self.line;
+
+ write!(f, "{file}:{line}: {library}: {reason} ({packed:#x})")
+ }
+}
+
+#[derive(Copy, Clone)]
+pub struct ErrorIterator {}
+
+impl Iterator for ErrorIterator {
+ type Item = Error;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ Self::Item::get()
+ }
+}
+
+pub type Result<T> = core::result::Result<T, ErrorIterator>;
+
+#[repr(transparent)]
+pub struct Aead(EVP_AEAD);
+
+impl Aead {
+ pub fn aes_256_gcm_randnonce() -> Option<&'static Self> {
+ // SAFETY - Returned pointer is checked below.
+ let aead = unsafe { EVP_aead_aes_256_gcm_randnonce() };
+ if aead.is_null() {
+ None
+ } else {
+ // SAFETY - We assume that the non-NULL value points to a valid and static EVP_AEAD.
+ Some(unsafe { &*(aead as *const _) })
+ }
+ }
+
+ pub fn max_overhead(&self) -> usize {
+ // SAFETY - Function should only read from self.
+ unsafe { EVP_AEAD_max_overhead(self.as_ref() as *const _) }
+ }
+}
+
+#[repr(transparent)]
+pub struct AeadCtx(EVP_AEAD_CTX);
+
+impl AeadCtx {
+ pub fn new_aes_256_gcm_randnonce(key: &[u8]) -> Result<Self> {
+ let aead = Aead::aes_256_gcm_randnonce().unwrap();
+
+ Self::new(aead, key)
+ }
+
+ fn new(aead: &'static Aead, key: &[u8]) -> Result<Self> {
+ const DEFAULT_TAG_LENGTH: usize = 0;
+ let engine = ptr::null_mut(); // Use default implementation.
+ let mut ctx = MaybeUninit::zeroed();
+ // SAFETY - Initialize the EVP_AEAD_CTX with const pointers to the AEAD and key.
+ let result = unsafe {
+ EVP_AEAD_CTX_init(
+ ctx.as_mut_ptr(),
+ aead.as_ref() as *const _,
+ key.as_ptr(),
+ key.len(),
+ DEFAULT_TAG_LENGTH,
+ engine,
+ )
+ };
+
+ if result == 1 {
+ // SAFETY - We assume that the non-NULL value points to a valid and static EVP_AEAD.
+ Ok(Self(unsafe { ctx.assume_init() }))
+ } else {
+ Err(ErrorIterator {})
+ }
+ }
+
+ pub fn aead(&self) -> Option<&'static Aead> {
+ // SAFETY - The function should only read from self.
+ let aead = unsafe { EVP_AEAD_CTX_aead(self.as_ref() as *const _) };
+ if aead.is_null() {
+ None
+ } else {
+ // SAFETY - We assume that the non-NULL value points to a valid and static EVP_AEAD.
+ Some(unsafe { &*(aead as *const _) })
+ }
+ }
+
+ pub fn open<'b>(&self, out: &'b mut [u8], data: &[u8]) -> Result<&'b mut [u8]> {
+ let nonce = ptr::null_mut();
+ let nonce_len = 0;
+ let ad = ptr::null_mut();
+ let ad_len = 0;
+ let mut out_len = MaybeUninit::uninit();
+ // SAFETY - The function should only read from self and write to out (at most the provided
+ // number of bytes) and out_len while reading from data (at most the provided number of
+ // bytes), ignoring any NULL input.
+ let result = unsafe {
+ EVP_AEAD_CTX_open(
+ self.as_ref() as *const _,
+ out.as_mut_ptr(),
+ out_len.as_mut_ptr(),
+ out.len(),
+ nonce,
+ nonce_len,
+ data.as_ptr(),
+ data.len(),
+ ad,
+ ad_len,
+ )
+ };
+
+ if result == 1 {
+ // SAFETY - Any value written to out_len could be a valid usize. The value itself is
+ // validated as being a proper slice length by panicking in the following indexing
+ // otherwise.
+ let out_len = unsafe { out_len.assume_init() };
+ Ok(&mut out[..out_len])
+ } else {
+ Err(ErrorIterator {})
+ }
+ }
+
+ pub fn seal<'b>(&self, out: &'b mut [u8], data: &[u8]) -> Result<&'b mut [u8]> {
+ let nonce = ptr::null_mut();
+ let nonce_len = 0;
+ let ad = ptr::null_mut();
+ let ad_len = 0;
+ let mut out_len = MaybeUninit::uninit();
+ // SAFETY - The function should only read from self and write to out (at most the provided
+ // number of bytes) while reading from data (at most the provided number of bytes),
+ // ignoring any NULL input.
+ let result = unsafe {
+ EVP_AEAD_CTX_seal(
+ self.as_ref() as *const _,
+ out.as_mut_ptr(),
+ out_len.as_mut_ptr(),
+ out.len(),
+ nonce,
+ nonce_len,
+ data.as_ptr(),
+ data.len(),
+ ad,
+ ad_len,
+ )
+ };
+
+ if result == 1 {
+ // SAFETY - Any value written to out_len could be a valid usize. The value itself is
+ // validated as being a proper slice length by panicking in the following indexing
+ // otherwise.
+ let out_len = unsafe { out_len.assume_init() };
+ Ok(&mut out[..out_len])
+ } else {
+ Err(ErrorIterator {})
+ }
+ }
+}
+
+/// Cast a C string pointer to a static non-mutable reference.
+///
+/// # Safety
+///
+/// The caller needs to ensure that the pointer points to a valid C string and that the C lifetime
+/// of the string is compatible with a static Rust lifetime.
+unsafe fn as_static_cstr(p: *const c_char) -> Option<&'static CStr> {
+ if p.is_null() {
+ None
+ } else {
+ Some(CStr::from_ptr(p))
+ }
+}
+
+impl AsRef<EVP_AEAD> for Aead {
+ fn as_ref(&self) -> &EVP_AEAD {
+ &self.0
+ }
+}
+
+impl AsRef<EVP_AEAD_CTX> for AeadCtx {
+ fn as_ref(&self) -> &EVP_AEAD_CTX {
+ &self.0
+ }
+}
+
+pub fn hkdf_sh512<const N: usize>(secret: &[u8], salt: &[u8], info: &[u8]) -> Result<[u8; N]> {
+ let mut key = [0; N];
+ // SAFETY - The function shouldn't access any Rust variable and the returned value is accepted
+ // as a potentially NULL pointer.
+ let digest = unsafe { EVP_sha512() };
+
+ assert!(!digest.is_null());
+ // SAFETY - Only reads from/writes to the provided slices and supports digest was checked not
+ // be NULL.
+ let result = unsafe {
+ HKDF(
+ key.as_mut_ptr(),
+ key.len(),
+ digest,
+ secret.as_ptr(),
+ secret.len(),
+ salt.as_ptr(),
+ salt.len(),
+ info.as_ptr(),
+ info.len(),
+ )
+ };
+
+ if result == 1 {
+ Ok(key)
+ } else {
+ Err(ErrorIterator {})
+ }
+}
diff --git a/pvmfw/src/debug_policy.rs b/pvmfw/src/debug_policy.rs
index 37e2af8..23d3e1d 100644
--- a/pvmfw/src/debug_policy.rs
+++ b/pvmfw/src/debug_policy.rs
@@ -14,7 +14,7 @@
//! Support for the debug policy overlay in pvmfw
-use alloc::vec;
+use alloc::{vec, vec::Vec};
use core::ffi::CStr;
use core::fmt;
use libfdt::FdtError;
@@ -63,12 +63,8 @@
fdt.pack().map_err(|e| DebugPolicyError::OverlaidFdt("Failed to re-pack", e))
}
-/// Dsiables ramdump by removing crashkernel from bootargs in /chosen.
-///
-/// # Safety
-///
-/// This may corrupt the input `Fdt` when error happens while editing prop value.
-unsafe fn disable_ramdump(fdt: &mut libfdt::Fdt) -> Result<(), DebugPolicyError> {
+/// Disables ramdump by removing crashkernel from bootargs in /chosen.
+fn disable_ramdump(fdt: &mut libfdt::Fdt) -> Result<(), DebugPolicyError> {
let chosen_path = CStr::from_bytes_with_nul(b"/chosen\0").unwrap();
let bootargs_name = CStr::from_bytes_with_nul(b"bootargs\0").unwrap();
@@ -129,6 +125,63 @@
}
}
+/// Enables console output by adding kernel.printk.devkmsg and kernel.console to bootargs.
+/// This uses hardcoded console name 'hvc0' and it should be match with microdroid's bootconfig.debuggable.
+fn enable_console_output(fdt: &mut libfdt::Fdt) -> Result<(), DebugPolicyError> {
+ let chosen_path = CStr::from_bytes_with_nul(b"/chosen\0").unwrap();
+ let bootargs_name = CStr::from_bytes_with_nul(b"bootargs\0").unwrap();
+
+ let chosen = match fdt
+ .node(chosen_path)
+ .map_err(|e| DebugPolicyError::Fdt("Failed to find /chosen", e))?
+ {
+ Some(node) => node,
+ None => return Ok(()),
+ };
+
+ let bootargs = match chosen
+ .getprop_str(bootargs_name)
+ .map_err(|e| DebugPolicyError::Fdt("Failed to find bootargs prop", e))?
+ {
+ Some(value) if !value.to_bytes().is_empty() => value,
+ _ => return Ok(()),
+ };
+
+ let mut new_bootargs = Vec::from(bootargs.to_bytes());
+ new_bootargs.extend_from_slice(b" printk.devkmsg=on console=hvc0\0");
+
+ // We'll set larger prop, and need to prepare some room first.
+ fdt.unpack().map_err(|e| DebugPolicyError::OverlaidFdt("Failed to unpack", e))?;
+
+ // We've checked existence of /chosen node at the beginning.
+ let mut chosen_mut = fdt.node_mut(chosen_path).unwrap().unwrap();
+ chosen_mut.setprop(bootargs_name, new_bootargs.as_slice()).map_err(|e| {
+ DebugPolicyError::OverlaidFdt("Failed to enabled console output. FDT might be corrupted", e)
+ })?;
+
+ fdt.pack().map_err(|e| DebugPolicyError::OverlaidFdt("Failed to pack", e))?;
+ Ok(())
+}
+
+/// Returns true only if fdt has log prop in the /avf/guest/common node with value <1>
+fn is_console_output_enabled(fdt: &libfdt::Fdt) -> Result<bool, DebugPolicyError> {
+ let common = match fdt
+ .node(CStr::from_bytes_with_nul(b"/avf/guest/common\0").unwrap())
+ .map_err(|e| DebugPolicyError::DebugPolicyFdt("Failed to find /avf/guest/common node", e))?
+ {
+ Some(node) => node,
+ None => return Ok(false),
+ };
+
+ match common
+ .getprop_u32(CStr::from_bytes_with_nul(b"log\0").unwrap())
+ .map_err(|e| DebugPolicyError::DebugPolicyFdt("Failed to find log prop", e))?
+ {
+ Some(1) => Ok(true),
+ _ => Ok(false),
+ }
+}
+
/// Handles debug policies.
///
/// # Safety
@@ -146,7 +199,14 @@
// Handles ramdump in the debug policy
if is_ramdump_enabled(fdt)? {
info!("ramdump is enabled by debug policy");
- return Ok(());
+ } else {
+ disable_ramdump(fdt)?;
}
- disable_ramdump(fdt)
+
+ // Handles console output in the debug policy
+ if is_console_output_enabled(fdt)? {
+ enable_console_output(fdt)?;
+ info!("console output is enabled by debug policy");
+ }
+ Ok(())
}
diff --git a/pvmfw/src/dice.rs b/pvmfw/src/dice.rs
index 14f522f..3ceb8ef 100644
--- a/pvmfw/src/dice.rs
+++ b/pvmfw/src/dice.rs
@@ -42,9 +42,9 @@
}
pub struct PartialInputs {
- code_hash: Hash,
- auth_hash: Hash,
- mode: DiceMode,
+ pub code_hash: Hash,
+ pub auth_hash: Hash,
+ pub mode: DiceMode,
}
impl PartialInputs {
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index 964ded1..106a4ef 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -22,6 +22,7 @@
use crate::memory::MemoryTracker;
use crate::mmio_guard;
use crate::mmu;
+use crate::rand;
use core::arch::asm;
use core::num::NonZeroUsize;
use core::slice;
@@ -241,7 +242,7 @@
RebootReason::InvalidConfig
})?;
- let bcc_slice = appended.get_bcc_mut();
+ let (bcc_slice, debug_policy) = appended.get_entries();
debug!("Activating dynamic page table...");
// SAFETY - page_table duplicates the static mappings for everything that the Rust code is
@@ -252,6 +253,11 @@
let mut memory = MemoryTracker::new(page_table);
let slices = MemorySlices::new(fdt, payload, payload_size, &mut memory)?;
+ rand::init().map_err(|e| {
+ error!("Failed to initialize rand: {e}");
+ RebootReason::InternalError
+ })?;
+
// This wrapper allows main() to be blissfully ignorant of platform details.
crate::main(slices.fdt, slices.kernel, slices.ramdisk, bcc_slice, &mut memory)?;
@@ -260,7 +266,7 @@
// SAFETY - As we `?` the result, there is no risk of using a bad `slices.fdt`.
unsafe {
- handle_debug_policy(slices.fdt, appended.get_debug_policy()).map_err(|e| {
+ handle_debug_policy(slices.fdt, debug_policy).map_err(|e| {
error!("Unexpected error when handling debug policy: {e:?}");
RebootReason::from(e)
})?;
@@ -391,17 +397,10 @@
}
}
- fn get_debug_policy(&mut self) -> Option<&mut [u8]> {
+ fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>) {
match self {
- Self::Config(ref mut cfg) => cfg.get_debug_policy(),
- Self::LegacyBcc(_) => None,
- }
- }
-
- fn get_bcc_mut(&mut self) -> &mut [u8] {
- match self {
- Self::LegacyBcc(ref mut bcc) => bcc,
- Self::Config(ref mut cfg) => cfg.get_bcc_mut(),
+ Self::Config(ref mut cfg) => cfg.get_entries(),
+ Self::LegacyBcc(ref mut bcc) => (bcc, None),
}
}
}
diff --git a/pvmfw/src/fdt.rs b/pvmfw/src/fdt.rs
index b735b9c..793eaac 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -16,6 +16,8 @@
use core::ffi::CStr;
use core::ops::Range;
+use libfdt::Fdt;
+use libfdt::FdtError;
/// Extract from /config the address range containing the pre-loaded kernel.
pub fn kernel_range(fdt: &libfdt::Fdt) -> libfdt::Result<Option<Range<usize>>> {
@@ -49,10 +51,35 @@
Ok(None)
}
-/// Add a "google,open-dice"-compatible reserved-memory node to the tree.
-pub fn add_dice_node(fdt: &mut libfdt::Fdt, addr: usize, size: usize) -> libfdt::Result<()> {
+/// Modifies the input DT according to the fields of the configuration.
+pub fn modify_for_next_stage(
+ fdt: &mut Fdt,
+ bcc: &[u8],
+ new_instance: bool,
+ strict_boot: bool,
+) -> libfdt::Result<()> {
fdt.unpack()?;
+ add_dice_node(fdt, bcc.as_ptr() as usize, bcc.len())?;
+
+ set_or_clear_chosen_flag(
+ fdt,
+ CStr::from_bytes_with_nul(b"avf,strict-boot\0").unwrap(),
+ strict_boot,
+ )?;
+ set_or_clear_chosen_flag(
+ fdt,
+ CStr::from_bytes_with_nul(b"avf,new-instance\0").unwrap(),
+ new_instance,
+ )?;
+
+ fdt.pack()?;
+
+ Ok(())
+}
+
+/// Add a "google,open-dice"-compatible reserved-memory node to the tree.
+fn add_dice_node(fdt: &mut Fdt, addr: usize, size: usize) -> libfdt::Result<()> {
let reserved_memory = CStr::from_bytes_with_nul(b"/reserved-memory\0").unwrap();
// We reject DTs with missing reserved-memory node as validation should have checked that the
// "swiotlb" subnode (compatible = "restricted-dma-pool") was present.
@@ -67,10 +94,25 @@
let no_map = CStr::from_bytes_with_nul(b"no-map\0").unwrap();
dice.appendprop(no_map, &[])?;
+ let addr = addr.try_into().unwrap();
+ let size = size.try_into().unwrap();
let reg = CStr::from_bytes_with_nul(b"reg\0").unwrap();
- dice.appendprop_addrrange(reg, addr as u64, size as u64)?;
+ dice.appendprop_addrrange(reg, addr, size)?;
- fdt.pack()?;
+ Ok(())
+}
+
+fn set_or_clear_chosen_flag(fdt: &mut Fdt, flag: &CStr, value: bool) -> libfdt::Result<()> {
+ // TODO(b/249054080): Refactor to not panic if the DT doesn't contain a /chosen node.
+ let mut chosen = fdt.chosen_mut()?.unwrap();
+ if value {
+ chosen.setprop_empty(flag)?;
+ } else {
+ match chosen.delprop(flag) {
+ Ok(()) | Err(FdtError::NotFound) => (),
+ Err(e) => return Err(e),
+ }
+ }
Ok(())
}
diff --git a/pvmfw/src/gpt.rs b/pvmfw/src/gpt.rs
new file mode 100644
index 0000000..6af3047
--- /dev/null
+++ b/pvmfw/src/gpt.rs
@@ -0,0 +1,253 @@
+// Copyright 2023, 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.
+
+//! Support for parsing GUID partition tables.
+
+use crate::helpers::ceiling_div;
+use crate::virtio::pci::VirtIOBlk;
+use core::cmp::min;
+use core::fmt;
+use core::mem::size_of;
+use core::ops::RangeInclusive;
+use core::slice;
+use static_assertions::const_assert;
+use static_assertions::const_assert_eq;
+use uuid::Uuid;
+use virtio_drivers::device::blk::SECTOR_SIZE;
+
+pub enum Error {
+ /// VirtIO error during read operation.
+ FailedRead(virtio_drivers::Error),
+ /// VirtIO error during write operation.
+ FailedWrite(virtio_drivers::Error),
+ /// Invalid GPT header.
+ InvalidHeader,
+ /// Invalid partition block index.
+ BlockOutsidePartition(usize),
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Self::FailedRead(e) => write!(f, "Failed to read from disk: {e}"),
+ Self::FailedWrite(e) => write!(f, "Failed to write to disk: {e}"),
+ Self::InvalidHeader => write!(f, "Found invalid GPT header"),
+ Self::BlockOutsidePartition(i) => write!(f, "Accessed invalid block index {i}"),
+ }
+ }
+}
+
+pub type Result<T> = core::result::Result<T, Error>;
+
+pub struct Partition {
+ partitions: Partitions,
+ indices: RangeInclusive<usize>,
+}
+
+impl Partition {
+ pub fn get_by_name(device: VirtIOBlk, name: &str) -> Result<Option<Self>> {
+ Partitions::new(device)?.get_partition_by_name(name)
+ }
+
+ fn new(partitions: Partitions, entry: &Entry) -> Self {
+ let first = entry.first_lba().try_into().unwrap();
+ let last = entry.last_lba().try_into().unwrap();
+
+ Self { partitions, indices: first..=last }
+ }
+
+ pub fn indices(&self) -> RangeInclusive<usize> {
+ self.indices.clone()
+ }
+
+ pub fn read_block(&mut self, index: usize, blk: &mut [u8]) -> Result<()> {
+ let index = self.block_index(index).ok_or(Error::BlockOutsidePartition(index))?;
+ self.partitions.read_block(index, blk)
+ }
+
+ pub fn write_block(&mut self, index: usize, blk: &[u8]) -> Result<()> {
+ let index = self.block_index(index).ok_or(Error::BlockOutsidePartition(index))?;
+ self.partitions.write_block(index, blk)
+ }
+
+ fn block_index(&self, index: usize) -> Option<usize> {
+ if self.indices.contains(&index) {
+ Some(index)
+ } else {
+ None
+ }
+ }
+}
+
+pub struct Partitions {
+ device: VirtIOBlk,
+ entries_count: usize,
+}
+
+impl Partitions {
+ pub const LBA_SIZE: usize = SECTOR_SIZE;
+
+ fn new(mut device: VirtIOBlk) -> Result<Self> {
+ let mut blk = [0; Self::LBA_SIZE];
+ device.read_block(Header::LBA, &mut blk).map_err(Error::FailedRead)?;
+ let (header_bytes, _) = blk.split_at(size_of::<Header>());
+ let header = Header::from_bytes(header_bytes).ok_or(Error::InvalidHeader)?;
+ let entries_count = usize::try_from(header.entries_count()).unwrap();
+
+ Ok(Self { device, entries_count })
+ }
+
+ fn get_partition_by_name(mut self, name: &str) -> Result<Option<Partition>> {
+ const_assert_eq!(Partitions::LBA_SIZE.rem_euclid(size_of::<Entry>()), 0);
+ let entries_per_blk = Partitions::LBA_SIZE.checked_div(size_of::<Entry>()).unwrap();
+
+ // Create a UTF-16 reference against which we'll compare partition names. Note that unlike
+ // the C99 wcslen(), this comparison will cover bytes past the first L'\0' character.
+ let mut needle = [0; Entry::NAME_SIZE / size_of::<u16>()];
+ for (dest, src) in needle.iter_mut().zip(name.encode_utf16()) {
+ *dest = src;
+ }
+
+ let mut blk = [0; Self::LBA_SIZE];
+ let mut rem = self.entries_count;
+ let num_blocks = ceiling_div(self.entries_count, entries_per_blk).unwrap();
+ for i in Header::ENTRIES_LBA..Header::ENTRIES_LBA.checked_add(num_blocks).unwrap() {
+ self.read_block(i, &mut blk)?;
+ let entries = blk.as_ptr().cast::<Entry>();
+ // SAFETY - blk is assumed to be properly aligned for Entry and its size is assert-ed
+ // above. All potential values of the slice will produce valid Entry values.
+ let entries = unsafe { slice::from_raw_parts(entries, min(rem, entries_per_blk)) };
+ for entry in entries {
+ let entry_name = entry.name;
+ if entry_name == needle {
+ return Ok(Some(Partition::new(self, entry)));
+ }
+ rem -= 1;
+ }
+ }
+ Ok(None)
+ }
+
+ fn read_block(&mut self, index: usize, blk: &mut [u8]) -> Result<()> {
+ self.device.read_block(index, blk).map_err(Error::FailedRead)
+ }
+
+ fn write_block(&mut self, index: usize, blk: &[u8]) -> Result<()> {
+ self.device.write_block(index, blk).map_err(Error::FailedWrite)
+ }
+}
+
+type Lba = u64;
+
+/// Structure as defined in release 2.10 of the UEFI Specification (5.3.2 GPT Header).
+#[repr(C, packed)]
+struct Header {
+ signature: u64,
+ revision: u32,
+ header_size: u32,
+ header_crc32: u32,
+ reserved0: u32,
+ current_lba: Lba,
+ backup_lba: Lba,
+ first_lba: Lba,
+ last_lba: Lba,
+ disk_guid: Uuid,
+ entries_lba: Lba,
+ entries_count: u32,
+ entry_size: u32,
+ entries_crc32: u32,
+}
+const_assert!(size_of::<Header>() < Partitions::LBA_SIZE);
+
+impl Header {
+ const SIGNATURE: u64 = u64::from_le_bytes(*b"EFI PART");
+ const REVISION_1_0: u32 = 1 << 16;
+ const LBA: usize = 1;
+ const ENTRIES_LBA: usize = 2;
+
+ fn from_bytes(bytes: &[u8]) -> Option<&Self> {
+ let bytes = bytes.get(..size_of::<Self>())?;
+ // SAFETY - We assume that bytes is properly aligned for Header and have verified above
+ // that it holds enough bytes. All potential values of the slice will produce a valid
+ // Header.
+ let header = unsafe { &*bytes.as_ptr().cast::<Self>() };
+
+ if header.is_valid() {
+ Some(header)
+ } else {
+ None
+ }
+ }
+
+ fn is_valid(&self) -> bool {
+ self.signature() == Self::SIGNATURE
+ && self.header_size() == size_of::<Self>().try_into().unwrap()
+ && self.revision() == Self::REVISION_1_0
+ && self.entry_size() == size_of::<Entry>().try_into().unwrap()
+ && self.current_lba() == Self::LBA.try_into().unwrap()
+ && self.entries_lba() == Self::ENTRIES_LBA.try_into().unwrap()
+ }
+
+ fn signature(&self) -> u64 {
+ u64::from_le(self.signature)
+ }
+
+ fn entries_count(&self) -> u32 {
+ u32::from_le(self.entries_count)
+ }
+
+ fn header_size(&self) -> u32 {
+ u32::from_le(self.header_size)
+ }
+
+ fn revision(&self) -> u32 {
+ u32::from_le(self.revision)
+ }
+
+ fn entry_size(&self) -> u32 {
+ u32::from_le(self.entry_size)
+ }
+
+ fn entries_lba(&self) -> Lba {
+ Lba::from_le(self.entries_lba)
+ }
+
+ fn current_lba(&self) -> Lba {
+ Lba::from_le(self.current_lba)
+ }
+}
+
+/// Structure as defined in release 2.10 of the UEFI Specification (5.3.3 GPT Partition Entry
+/// Array).
+#[repr(C, packed)]
+struct Entry {
+ type_guid: Uuid,
+ guid: Uuid,
+ first_lba: Lba,
+ last_lba: Lba,
+ flags: u64,
+ name: [u16; Entry::NAME_SIZE / size_of::<u16>()], // UTF-16
+}
+
+impl Entry {
+ const NAME_SIZE: usize = 72;
+
+ fn first_lba(&self) -> Lba {
+ Lba::from_le(self.first_lba)
+ }
+
+ fn last_lba(&self) -> Lba {
+ Lba::from_le(self.last_lba)
+ }
+}
diff --git a/pvmfw/src/helpers.rs b/pvmfw/src/helpers.rs
index 40266f7..e6e3406 100644
--- a/pvmfw/src/helpers.rs
+++ b/pvmfw/src/helpers.rs
@@ -47,6 +47,17 @@
}
}
+/// Performs an integer division rounding up.
+///
+/// Note: Returns None if den isn't a power of two.
+pub const fn ceiling_div(num: usize, den: usize) -> Option<usize> {
+ let Some(r) = align_up(num, den) else {
+ return None;
+ };
+
+ r.checked_div(den)
+}
+
/// Aligns the given address to the given alignment, if it is a power of two.
///
/// Returns `None` if the alignment isn't a power of two.
diff --git a/pvmfw/src/hvc.rs b/pvmfw/src/hvc.rs
index dc99303..319ff9d 100644
--- a/pvmfw/src/hvc.rs
+++ b/pvmfw/src/hvc.rs
@@ -14,9 +14,19 @@
//! Wrappers around calls to the hypervisor.
+pub mod trng;
+
use crate::smccc::{self, checked_hvc64, checked_hvc64_expect_zero};
use log::info;
+const ARM_SMCCC_TRNG_VERSION: u32 = 0x8400_0050;
+#[allow(dead_code)]
+const ARM_SMCCC_TRNG_FEATURES: u32 = 0x8400_0051;
+#[allow(dead_code)]
+const ARM_SMCCC_TRNG_GET_UUID: u32 = 0x8400_0052;
+#[allow(dead_code)]
+const ARM_SMCCC_TRNG_RND32: u32 = 0x8400_0053;
+const ARM_SMCCC_TRNG_RND64: u32 = 0xc400_0053;
const ARM_SMCCC_KVM_FUNC_HYP_MEMINFO: u32 = 0xc6000002;
const ARM_SMCCC_KVM_FUNC_MEM_SHARE: u32 = 0xc6000003;
const ARM_SMCCC_KVM_FUNC_MEM_UNSHARE: u32 = 0xc6000004;
@@ -94,3 +104,22 @@
x => x,
}
}
+
+/// Returns the (major, minor) version tuple, as defined by the SMCCC TRNG.
+pub fn trng_version() -> trng::Result<(u16, u16)> {
+ let args = [0u64; 17];
+
+ let version = trng::hvc64(ARM_SMCCC_TRNG_VERSION, args)?[0];
+ Ok(((version >> 16) as u16, version as u16))
+}
+
+pub type TrngRng64Entropy = (u64, u64, u64);
+
+pub fn trng_rnd64(nbits: u64) -> trng::Result<TrngRng64Entropy> {
+ let mut args = [0u64; 17];
+ args[0] = nbits;
+
+ let regs = trng::hvc64_expect_zero(ARM_SMCCC_TRNG_RND64, args)?;
+
+ Ok((regs[1], regs[2], regs[3]))
+}
diff --git a/pvmfw/src/hvc/trng.rs b/pvmfw/src/hvc/trng.rs
new file mode 100644
index 0000000..d347693
--- /dev/null
+++ b/pvmfw/src/hvc/trng.rs
@@ -0,0 +1,65 @@
+// Copyright 2023, 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.
+
+use crate::smccc;
+use core::fmt;
+use core::result;
+
+/// Standard SMCCC TRNG error values as described in DEN 0098 1.0 REL0.
+#[derive(Debug, Clone)]
+pub enum Error {
+ /// The call is not supported by the implementation.
+ NotSupported,
+ /// One of the call parameters has a non-supported value.
+ InvalidParameter,
+ /// Call returned without the requested entropy bits.
+ NoEntropy,
+ /// Negative values indicate error.
+ Unknown(i64),
+ /// The call returned a positive value when 0 was expected.
+ Unexpected(u64),
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Self::NotSupported => write!(f, "SMCCC TRNG call not supported"),
+ Self::InvalidParameter => write!(f, "SMCCC TRNG call received non-supported value"),
+ Self::NoEntropy => write!(f, "SMCCC TRNG call returned no entropy"),
+ Self::Unexpected(v) => write!(f, "Unexpected SMCCC TRNG return value {} ({0:#x})", v),
+ Self::Unknown(e) => write!(f, "Unknown SMCCC TRNG return value {} ({0:#x})", e),
+ }
+ }
+}
+
+pub type Result<T> = result::Result<T, Error>;
+
+pub fn hvc64(function: u32, args: [u64; 17]) -> Result<[u64; 18]> {
+ let res = smccc::hvc64(function, args);
+ match res[0] as i64 {
+ ret if ret >= 0 => Ok(res),
+ -1 => Err(Error::NotSupported),
+ -2 => Err(Error::InvalidParameter),
+ -3 => Err(Error::NoEntropy),
+ ret => Err(Error::Unknown(ret)),
+ }
+}
+
+pub fn hvc64_expect_zero(function: u32, args: [u64; 17]) -> Result<[u64; 18]> {
+ let res = hvc64(function, args)?;
+ match res[0] {
+ 0 => Ok(res),
+ v => Err(Error::Unexpected(v)),
+ }
+}
diff --git a/pvmfw/src/instance.rs b/pvmfw/src/instance.rs
new file mode 100644
index 0000000..fbf2040
--- /dev/null
+++ b/pvmfw/src/instance.rs
@@ -0,0 +1,338 @@
+// Copyright 2023, 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.
+
+//! Support for reading and writing to the instance.img.
+
+use crate::crypto;
+use crate::crypto::hkdf_sh512;
+use crate::crypto::AeadCtx;
+use crate::dice::PartialInputs;
+use crate::gpt;
+use crate::gpt::Partition;
+use crate::gpt::Partitions;
+use crate::helpers::ceiling_div;
+use crate::rand;
+use crate::virtio::pci::VirtIOBlkIterator;
+use core::fmt;
+use core::mem::size_of;
+use core::slice;
+use diced_open_dice::DiceMode;
+use diced_open_dice::Hash;
+use diced_open_dice::Hidden;
+use log::trace;
+use uuid::Uuid;
+use virtio_drivers::transport::pci::bus::PciRoot;
+
+pub enum Error {
+ /// Unexpected I/O error while accessing the underlying disk.
+ FailedIo(gpt::Error),
+ /// Failed to decrypt the entry.
+ FailedOpen(crypto::ErrorIterator),
+ /// Failed to generate a random salt to be stored.
+ FailedSaltGeneration(rand::Error),
+ /// Failed to encrypt the entry.
+ FailedSeal(crypto::ErrorIterator),
+ /// Impossible to create a new instance.img entry.
+ InstanceImageFull,
+ /// Badly formatted instance.img header block.
+ InvalidInstanceImageHeader,
+ /// No instance.img ("vm-instance") partition found.
+ MissingInstanceImage,
+ /// The instance.img doesn't contain a header.
+ MissingInstanceImageHeader,
+ /// Authority hash found in the pvmfw instance.img entry doesn't match the trusted public key.
+ RecordedAuthHashMismatch,
+ /// Code hash found in the pvmfw instance.img entry doesn't match the inputs.
+ RecordedCodeHashMismatch,
+ /// DICE mode found in the pvmfw instance.img entry doesn't match the current one.
+ RecordedDiceModeMismatch,
+ /// Size of the instance.img entry being read or written is not supported.
+ UnsupportedEntrySize(usize),
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Self::FailedIo(e) => write!(f, "Failed I/O to disk: {e}"),
+ Self::FailedOpen(e_iter) => {
+ writeln!(f, "Failed to open the instance.img partition:")?;
+ for e in *e_iter {
+ writeln!(f, "\t{e}")?;
+ }
+ Ok(())
+ }
+ Self::FailedSaltGeneration(e) => write!(f, "Failed to generate salt: {e}"),
+ Self::FailedSeal(e_iter) => {
+ writeln!(f, "Failed to seal the instance.img partition:")?;
+ for e in *e_iter {
+ writeln!(f, "\t{e}")?;
+ }
+ Ok(())
+ }
+ Self::InstanceImageFull => write!(f, "Failed to obtain a free instance.img partition"),
+ Self::InvalidInstanceImageHeader => write!(f, "instance.img header is invalid"),
+ Self::MissingInstanceImage => write!(f, "Failed to find the instance.img partition"),
+ Self::MissingInstanceImageHeader => write!(f, "instance.img header is missing"),
+ Self::RecordedAuthHashMismatch => write!(f, "Recorded authority hash doesn't match"),
+ Self::RecordedCodeHashMismatch => write!(f, "Recorded code hash doesn't match"),
+ Self::RecordedDiceModeMismatch => write!(f, "Recorded DICE mode doesn't match"),
+ Self::UnsupportedEntrySize(sz) => write!(f, "Invalid entry size: {sz}"),
+ }
+ }
+}
+
+pub type Result<T> = core::result::Result<T, Error>;
+
+pub fn get_or_generate_instance_salt(
+ pci_root: &mut PciRoot,
+ dice_inputs: &PartialInputs,
+ secret: &[u8],
+) -> Result<(bool, Hidden)> {
+ let mut instance_img = find_instance_img(pci_root)?;
+
+ let entry = locate_entry(&mut instance_img)?;
+ trace!("Found pvmfw instance.img entry: {entry:?}");
+
+ let key = hkdf_sh512::<32>(secret, /*salt=*/ &[], b"vm-instance");
+ let mut blk = [0; BLK_SIZE];
+ match entry {
+ PvmfwEntry::Existing { header_index, payload_size } => {
+ if payload_size > blk.len() {
+ // We currently only support single-blk entries.
+ return Err(Error::UnsupportedEntrySize(payload_size));
+ }
+ let payload_index = header_index + 1;
+ instance_img.read_block(payload_index, &mut blk).map_err(Error::FailedIo)?;
+
+ let payload = &blk[..payload_size];
+ let mut entry = [0; size_of::<EntryBody>()];
+ let key = key.map_err(Error::FailedOpen)?;
+ let aead = AeadCtx::new_aes_256_gcm_randnonce(&key).map_err(Error::FailedOpen)?;
+ let decrypted = aead.open(&mut entry, payload).map_err(Error::FailedOpen)?;
+
+ let body: &EntryBody = decrypted.as_ref();
+ if body.code_hash != dice_inputs.code_hash {
+ Err(Error::RecordedCodeHashMismatch)
+ } else if body.auth_hash != dice_inputs.auth_hash {
+ Err(Error::RecordedAuthHashMismatch)
+ } else if body.mode() != dice_inputs.mode {
+ Err(Error::RecordedDiceModeMismatch)
+ } else {
+ Ok((false, body.salt))
+ }
+ }
+ PvmfwEntry::New { header_index } => {
+ let salt = rand::random_array().map_err(Error::FailedSaltGeneration)?;
+ let entry_body = EntryBody::new(dice_inputs, &salt);
+ let body = entry_body.as_ref();
+
+ let key = key.map_err(Error::FailedSeal)?;
+ let aead = AeadCtx::new_aes_256_gcm_randnonce(&key).map_err(Error::FailedSeal)?;
+ // We currently only support single-blk entries.
+ assert!(body.len() + aead.aead().unwrap().max_overhead() < blk.len());
+ let encrypted = aead.seal(&mut blk, body).map_err(Error::FailedSeal)?;
+ let payload_size = encrypted.len();
+ let payload_index = header_index + 1;
+ instance_img.write_block(payload_index, &blk).map_err(Error::FailedIo)?;
+
+ let header = EntryHeader::new(PvmfwEntry::UUID, payload_size);
+ let (blk_header, blk_rest) = blk.split_at_mut(size_of::<EntryHeader>());
+ blk_header.copy_from_slice(header.as_ref());
+ blk_rest.fill(0);
+ instance_img.write_block(header_index, &blk).map_err(Error::FailedIo)?;
+
+ Ok((true, salt))
+ }
+ }
+}
+
+#[repr(C, packed)]
+struct Header {
+ magic: [u8; Header::MAGIC.len()],
+ version: u16,
+}
+
+impl Header {
+ const MAGIC: &[u8] = b"Android-VM-instance";
+ const VERSION_1: u16 = 1;
+
+ pub fn is_valid(&self) -> bool {
+ self.magic == Self::MAGIC && self.version() == Self::VERSION_1
+ }
+
+ fn version(&self) -> u16 {
+ u16::from_le(self.version)
+ }
+
+ fn from_bytes(bytes: &[u8]) -> Option<&Self> {
+ let header: &Self = bytes.as_ref();
+
+ if header.is_valid() {
+ Some(header)
+ } else {
+ None
+ }
+ }
+}
+
+impl AsRef<Header> for [u8] {
+ fn as_ref(&self) -> &Header {
+ // SAFETY - Assume that the alignement and size match Header.
+ unsafe { &*self.as_ptr().cast::<Header>() }
+ }
+}
+
+fn find_instance_img(pci_root: &mut PciRoot) -> Result<Partition> {
+ for device in VirtIOBlkIterator::new(pci_root) {
+ match Partition::get_by_name(device, "vm-instance") {
+ Ok(Some(p)) => return Ok(p),
+ Ok(None) => {}
+ Err(e) => log::warn!("error while reading from disk: {e}"),
+ };
+ }
+
+ Err(Error::MissingInstanceImage)
+}
+
+#[derive(Debug)]
+enum PvmfwEntry {
+ Existing { header_index: usize, payload_size: usize },
+ New { header_index: usize },
+}
+
+const BLK_SIZE: usize = Partitions::LBA_SIZE;
+
+impl PvmfwEntry {
+ const UUID: Uuid = Uuid::from_u128(0x90d2174a038a4bc6adf3824848fc5825);
+}
+
+fn locate_entry(partition: &mut Partition) -> Result<PvmfwEntry> {
+ let mut blk = [0; BLK_SIZE];
+ let mut indices = partition.indices();
+ let header_index = indices.next().ok_or(Error::MissingInstanceImageHeader)?;
+ partition.read_block(header_index, &mut blk).map_err(Error::FailedIo)?;
+ // The instance.img header is only used for discovery/validation.
+ let _ = Header::from_bytes(&blk).ok_or(Error::InvalidInstanceImageHeader)?;
+
+ while let Some(header_index) = indices.next() {
+ partition.read_block(header_index, &mut blk).map_err(Error::FailedIo)?;
+
+ let header: &EntryHeader = blk[..size_of::<EntryHeader>()].as_ref();
+ match (header.uuid(), header.payload_size()) {
+ (uuid, _) if uuid.is_nil() => return Ok(PvmfwEntry::New { header_index }),
+ (PvmfwEntry::UUID, payload_size) => {
+ return Ok(PvmfwEntry::Existing { header_index, payload_size })
+ }
+ (uuid, payload_size) => {
+ trace!("Skipping instance.img entry {uuid}: {payload_size:?} bytes");
+ let n = ceiling_div(payload_size, BLK_SIZE).unwrap();
+ if n > 0 {
+ let _ = indices.nth(n - 1); // consume
+ }
+ }
+ };
+ }
+
+ Err(Error::InstanceImageFull)
+}
+
+/// Marks the start of an instance.img entry.
+///
+/// Note: Virtualization/microdroid_manager/src/instance.rs uses the name "partition".
+#[repr(C)]
+struct EntryHeader {
+ uuid: u128,
+ payload_size: u64,
+}
+
+impl EntryHeader {
+ fn new(uuid: Uuid, payload_size: usize) -> Self {
+ Self { uuid: uuid.as_u128(), payload_size: u64::try_from(payload_size).unwrap().to_le() }
+ }
+
+ fn uuid(&self) -> Uuid {
+ Uuid::from_u128(self.uuid)
+ }
+
+ fn payload_size(&self) -> usize {
+ usize::try_from(u64::from_le(self.payload_size)).unwrap()
+ }
+}
+
+impl AsRef<EntryHeader> for [u8] {
+ fn as_ref(&self) -> &EntryHeader {
+ assert_eq!(self.len(), size_of::<EntryHeader>());
+ // SAFETY - The size of the slice was checked and any value may be considered valid.
+ unsafe { &*self.as_ptr().cast::<EntryHeader>() }
+ }
+}
+
+impl AsRef<[u8]> for EntryHeader {
+ fn as_ref(&self) -> &[u8] {
+ let s = self as *const Self;
+ // SAFETY - Transmute the (valid) bytes into a slice.
+ unsafe { slice::from_raw_parts(s.cast::<u8>(), size_of::<Self>()) }
+ }
+}
+
+#[repr(C)]
+struct EntryBody {
+ code_hash: Hash,
+ auth_hash: Hash,
+ salt: Hidden,
+ mode: u8,
+}
+
+impl EntryBody {
+ fn new(dice_inputs: &PartialInputs, salt: &Hidden) -> Self {
+ let mode = match dice_inputs.mode {
+ DiceMode::kDiceModeNotInitialized => 0,
+ DiceMode::kDiceModeNormal => 1,
+ DiceMode::kDiceModeDebug => 2,
+ DiceMode::kDiceModeMaintenance => 3,
+ };
+
+ Self {
+ code_hash: dice_inputs.code_hash,
+ auth_hash: dice_inputs.auth_hash,
+ salt: *salt,
+ mode,
+ }
+ }
+
+ fn mode(&self) -> DiceMode {
+ match self.mode {
+ 1 => DiceMode::kDiceModeNormal,
+ 2 => DiceMode::kDiceModeDebug,
+ 3 => DiceMode::kDiceModeMaintenance,
+ _ => DiceMode::kDiceModeNotInitialized,
+ }
+ }
+}
+
+impl AsRef<EntryBody> for [u8] {
+ fn as_ref(&self) -> &EntryBody {
+ assert_eq!(self.len(), size_of::<EntryBody>());
+ // SAFETY - The size of the slice was checked and members are validated by accessors.
+ unsafe { &*self.as_ptr().cast::<EntryBody>() }
+ }
+}
+
+impl AsRef<[u8]> for EntryBody {
+ fn as_ref(&self) -> &[u8] {
+ let s = self as *const Self;
+ // SAFETY - Transmute the (valid) bytes into a slice.
+ unsafe { slice::from_raw_parts(s.cast::<u8>(), size_of::<Self>()) }
+ }
+}
diff --git a/pvmfw/src/main.rs b/pvmfw/src/main.rs
index 2e56597..d89e718 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -21,32 +21,37 @@
extern crate alloc;
mod config;
+mod crypto;
mod debug_policy;
mod dice;
mod entry;
mod exceptions;
mod fdt;
+mod gpt;
mod heap;
mod helpers;
mod hvc;
+mod instance;
mod memory;
mod mmio_guard;
mod mmu;
+mod rand;
mod smccc;
mod virtio;
use alloc::boxed::Box;
-use crate::{
- dice::PartialInputs,
- entry::RebootReason,
- fdt::add_dice_node,
- helpers::flush,
- helpers::GUEST_PAGE_SIZE,
- memory::MemoryTracker,
- virtio::pci::{self, find_virtio_devices},
-};
-use diced_open_dice::{bcc_handover_main_flow, bcc_handover_parse, HIDDEN_SIZE};
+use crate::dice::PartialInputs;
+use crate::entry::RebootReason;
+use crate::fdt::modify_for_next_stage;
+use crate::helpers::flush;
+use crate::helpers::GUEST_PAGE_SIZE;
+use crate::instance::get_or_generate_instance_salt;
+use crate::memory::MemoryTracker;
+use crate::virtio::pci;
+use diced_open_dice::bcc_handover_main_flow;
+use diced_open_dice::bcc_handover_parse;
+use diced_open_dice::DiceArtifacts;
use fdtpci::{PciError, PciInfo};
use libfdt::Fdt;
use log::{debug, error, info, trace};
@@ -81,7 +86,6 @@
let pci_info = PciInfo::from_fdt(fdt).map_err(handle_pci_error)?;
debug!("PCI: {:#x?}", pci_info);
let mut pci_root = pci::initialise(pci_info, memory)?;
- find_virtio_devices(&mut pci_root).map_err(handle_pci_error)?;
let verified_boot_data = verify_payload(signed_kernel, ramdisk, PUBLIC_KEY).map_err(|e| {
error!("Failed to verify the payload: {e}");
@@ -99,7 +103,14 @@
error!("Failed to compute partial DICE inputs: {e:?}");
RebootReason::InternalError
})?;
- let salt = [0; HIDDEN_SIZE]; // TODO(b/249723852): Get from instance.img and/or TRNG.
+ let cdi_seal = DiceArtifacts::cdi_seal(&bcc_handover);
+ let (new_instance, salt) = get_or_generate_instance_salt(&mut pci_root, &dice_inputs, cdi_seal)
+ .map_err(|e| {
+ error!("Failed to get instance.img salt: {e}");
+ RebootReason::InternalError
+ })?;
+ trace!("Got salt from instance.img: {salt:x?}");
+
let dice_inputs = dice_inputs.into_input_values(&salt).map_err(|e| {
error!("Failed to generate DICE inputs: {e:?}");
RebootReason::InternalError
@@ -110,8 +121,9 @@
})?;
flush(next_bcc);
- add_dice_node(fdt, next_bcc.as_ptr() as usize, NEXT_BCC_SIZE).map_err(|e| {
- error!("Failed to add DICE node to device tree: {e}");
+ let strict_boot = true;
+ modify_for_next_stage(fdt, next_bcc, new_instance, strict_boot).map_err(|e| {
+ error!("Failed to configure device tree: {e}");
RebootReason::InternalError
})?;
diff --git a/pvmfw/src/rand.rs b/pvmfw/src/rand.rs
new file mode 100644
index 0000000..a53cac6
--- /dev/null
+++ b/pvmfw/src/rand.rs
@@ -0,0 +1,99 @@
+// Copyright 2023, 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.
+
+use crate::hvc;
+use core::fmt;
+use core::mem::size_of;
+
+pub enum Error {
+ /// Error during SMCCC TRNG call.
+ Trng(hvc::trng::Error),
+ /// Unsupported SMCCC TRNG version.
+ UnsupportedVersion((u16, u16)),
+}
+
+impl From<hvc::trng::Error> for Error {
+ fn from(e: hvc::trng::Error) -> Self {
+ Self::Trng(e)
+ }
+}
+
+pub type Result<T> = core::result::Result<T, Error>;
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Self::Trng(e) => write!(f, "SMCCC TRNG error: {e}"),
+ Self::UnsupportedVersion((x, y)) => {
+ write!(f, "Unsupported SMCCC TRNG version v{x}.{y}")
+ }
+ }
+ }
+}
+
+/// Configure the source of entropy.
+pub fn init() -> Result<()> {
+ match hvc::trng_version()? {
+ (1, _) => Ok(()),
+ version => Err(Error::UnsupportedVersion(version)),
+ }
+}
+
+fn fill_with_entropy(s: &mut [u8]) -> Result<()> {
+ const MAX_BYTES_PER_CALL: usize = size_of::<hvc::TrngRng64Entropy>();
+ let bits = usize::try_from(u8::BITS).unwrap();
+
+ let (aligned, remainder) = s.split_at_mut(s.len() - s.len() % MAX_BYTES_PER_CALL);
+
+ for chunk in aligned.chunks_exact_mut(MAX_BYTES_PER_CALL) {
+ let (r, s, t) = hvc::trng_rnd64((chunk.len() * bits).try_into().unwrap())?;
+
+ let mut words = chunk.chunks_exact_mut(size_of::<u64>());
+ words.next().unwrap().clone_from_slice(&t.to_ne_bytes());
+ words.next().unwrap().clone_from_slice(&s.to_ne_bytes());
+ words.next().unwrap().clone_from_slice(&r.to_ne_bytes());
+ }
+
+ if !remainder.is_empty() {
+ let mut entropy = [0; MAX_BYTES_PER_CALL];
+ let (r, s, t) = hvc::trng_rnd64((remainder.len() * bits).try_into().unwrap())?;
+
+ let mut words = entropy.chunks_exact_mut(size_of::<u64>());
+ words.next().unwrap().clone_from_slice(&t.to_ne_bytes());
+ words.next().unwrap().clone_from_slice(&s.to_ne_bytes());
+ words.next().unwrap().clone_from_slice(&r.to_ne_bytes());
+
+ remainder.clone_from_slice(&entropy[..remainder.len()]);
+ }
+
+ Ok(())
+}
+
+pub fn random_array<const N: usize>() -> Result<[u8; N]> {
+ let mut arr = [0; N];
+ fill_with_entropy(&mut arr)?;
+ Ok(arr)
+}
+
+#[no_mangle]
+extern "C" fn CRYPTO_sysrand_for_seed(out: *mut u8, req: usize) {
+ CRYPTO_sysrand(out, req)
+}
+
+#[no_mangle]
+extern "C" fn CRYPTO_sysrand(out: *mut u8, req: usize) {
+ // SAFETY - We need to assume that out points to valid memory of size req.
+ let s = unsafe { core::slice::from_raw_parts_mut(out, req) };
+ let _ = fill_with_entropy(s);
+}
diff --git a/pvmfw/src/smccc.rs b/pvmfw/src/smccc.rs
index f92c076..ccf2680 100644
--- a/pvmfw/src/smccc.rs
+++ b/pvmfw/src/smccc.rs
@@ -16,7 +16,7 @@
// TODO(b/245889995): use psci-0.1.1 crate
#[inline(always)]
-fn hvc64(function: u32, args: [u64; 17]) -> [u64; 18] {
+pub fn hvc64(function: u32, args: [u64; 17]) -> [u64; 18] {
#[cfg(target_arch = "aarch64")]
unsafe {
let mut ret = [0; 18];
diff --git a/pvmfw/src/virtio/pci.rs b/pvmfw/src/virtio/pci.rs
index b61403b..58bc07e 100644
--- a/pvmfw/src/virtio/pci.rs
+++ b/pvmfw/src/virtio/pci.rs
@@ -17,11 +17,11 @@
use super::hal::HalImpl;
use crate::{entry::RebootReason, memory::MemoryTracker};
use alloc::boxed::Box;
-use fdtpci::{PciError, PciInfo};
-use log::{debug, error, info};
+use fdtpci::PciInfo;
+use log::{debug, error};
use once_cell::race::OnceBox;
use virtio_drivers::{
- device::blk::VirtIOBlk,
+ device::blk,
transport::{
pci::{
bus::{BusDeviceIterator, PciRoot},
@@ -69,7 +69,9 @@
Ok(())
}
-struct VirtIOBlkIterator<'a> {
+pub type VirtIOBlk = blk::VirtIOBlk<HalImpl, PciTransport>;
+
+pub struct VirtIOBlkIterator<'a> {
pci_root: &'a mut PciRoot,
bus: BusDeviceIterator,
}
@@ -82,7 +84,7 @@
}
impl<'a> Iterator for VirtIOBlkIterator<'a> {
- type Item = VirtIOBlk<HalImpl, PciTransport>;
+ type Item = VirtIOBlk;
fn next(&mut self) -> Option<Self::Item> {
loop {
@@ -112,14 +114,3 @@
}
}
}
-
-/// Finds VirtIO PCI devices.
-pub fn find_virtio_devices(pci_root: &mut PciRoot) -> Result<(), PciError> {
- for mut blk in VirtIOBlkIterator::new(pci_root) {
- info!("Found {} KiB block device.", blk.capacity() * 512 / 1024);
- let mut data = [0; 512];
- blk.read_block(0, &mut data).expect("Failed to read block device");
- }
-
- Ok(())
-}
diff --git a/tests/aidl/Android.bp b/tests/aidl/Android.bp
index d59ca7e..ed4e8ff 100644
--- a/tests/aidl/Android.bp
+++ b/tests/aidl/Android.bp
@@ -6,6 +6,10 @@
name: "com.android.microdroid.testservice",
srcs: ["com/android/microdroid/testservice/**/*.aidl"],
unstable: true,
+ flags: [
+ "-Werror",
+ "-Wno-mixed-oneway",
+ ],
backend: {
java: {
gen_rpc: true,
diff --git a/tests/aidl/com/android/microdroid/testservice/IAppCallback.aidl b/tests/aidl/com/android/microdroid/testservice/IAppCallback.aidl
new file mode 100644
index 0000000..9859090
--- /dev/null
+++ b/tests/aidl/com/android/microdroid/testservice/IAppCallback.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 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.testservice;
+
+import com.android.microdroid.testservice.IVmCallback;
+
+/**
+ * An interface exposed by the app for callbacks from the VM.
+ *
+ * {@hide}
+ */
+interface IAppCallback {
+ /** Invites the app to call vmCallback#echoMessage() */
+ void setVmCallback(IVmCallback vmCallback);
+
+ /** Asynchronusly called by the VM in response to a call to echoMessage(). */
+ void onEchoRequestReceived(String message);
+}
diff --git a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
index 1eda67e..c8c8660 100644
--- a/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/IBenchmarkService.aidl
@@ -41,7 +41,4 @@
/** Runs the vsock server on VM and receives data. */
void runVsockServerAndReceiveData(int serverFd, int numBytesToReceive);
-
- /** Adds two numbers and returns the result. */
- int add(int a, int b);
}
diff --git a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index 4611134..36c3aaf 100644
--- a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -15,7 +15,12 @@
*/
package com.android.microdroid.testservice;
-/** {@hide} */
+import com.android.microdroid.testservice.IAppCallback;
+
+/**
+ * This is the service exposed by the test payload, called by the test app.
+ * {@hide}
+ */
interface ITestService {
const long SERVICE_PORT = 5678;
@@ -56,6 +61,15 @@
/* get the content of the specified file. */
String readFromFile(String path);
+ /* get file permissions of the give file by stat'ing it */
+ int getFilePermissions(String path);
+
+ /** Returns flags for the given mountPoint. */
+ int getMountFlags(String mountPoint);
+
+ /** Requests the VM to asynchronously call appCallback.setVmCallback() */
+ void requestCallback(IAppCallback appCallback);
+
/**
* Request the service to exit, triggering the termination of the VM. This may cause any
* requests in flight to fail.
diff --git a/tests/aidl/com/android/microdroid/testservice/IVmCallback.aidl b/tests/aidl/com/android/microdroid/testservice/IVmCallback.aidl
new file mode 100644
index 0000000..617d184
--- /dev/null
+++ b/tests/aidl/com/android/microdroid/testservice/IVmCallback.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 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.testservice;
+
+/**
+ * An interface exposed by the VM for callbacks from the app.
+ *
+ * {@hide}
+ */
+interface IVmCallback {
+ /** Requests the VM to asynchronously call the app's onEchoRequestReceived() callback. */
+ void echoMessage(String message);
+}
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index 9d2b6c7..dac4993 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -19,6 +19,7 @@
jni_libs: [
"MicrodroidBenchmarkNativeLib",
"MicrodroidIdleNativeLib",
+ "MicrodroidTestNativeLib",
"libiovsock_host_jni",
],
jni_uses_platform_apis: true,
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 fd0158b..4b11d77 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -29,6 +29,8 @@
import android.app.Instrumentation;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
+import android.os.ParcelFileDescriptor.AutoCloseInputStream;
+import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.os.Process;
import android.os.RemoteException;
import android.system.virtualmachine.VirtualMachine;
@@ -40,6 +42,7 @@
import com.android.microdroid.test.common.ProcessUtil;
import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
import com.android.microdroid.testservice.IBenchmarkService;
+import com.android.microdroid.testservice.ITestService;
import org.junit.Before;
import org.junit.Rule;
@@ -48,8 +51,14 @@
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
@@ -57,7 +66,6 @@
import java.util.List;
import java.util.Map;
import java.util.OptionalLong;
-import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
@@ -72,7 +80,8 @@
private static final String APEX_ETC_FS = "/apex/com.android.virt/etc/fs/";
private static final double SIZE_MB = 1024.0 * 1024.0;
- private static final double NANO_TO_MILLI = 1000000.0;
+ private static final double NANO_TO_MILLI = 1_000_000.0;
+ private static final double NANO_TO_MICRO = 1_000.0;
private static final String MICRODROID_IMG_PREFIX = "microdroid_";
private static final String MICRODROID_IMG_SUFFIX = ".img";
@@ -569,64 +578,113 @@
}
@Test
- public void testVsockRpcBinderLatency() throws Exception {
+ public void testRpcBinderLatency() throws Exception {
+ final int NUM_WARMUPS = 10;
+ final int NUM_REQUESTS = 10_000;
+
VirtualMachineConfig config =
newVmConfigBuilder()
- .setPayloadConfigPath("assets/vm_config_io.json")
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_NONE)
.build();
- List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT);
+ List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS);
for (int i = 0; i < IO_TEST_TRIAL_COUNT; ++i) {
- String vmName = "test_vm_request_" + i;
- VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
- BenchmarkVmListener.create(new VsockRpcBinderLatencyListener(requestLatencies))
- .runToFinish(TAG, vm);
+ VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_latency" + i, config);
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (ts, tr) -> {
+ // Correctness check
+ tr.mAddInteger = ts.addInteger(123, 456);
+
+ // Warmup
+ for (int j = 0; j < NUM_WARMUPS; j++) {
+ ts.addInteger(j, j + 1);
+ }
+
+ // Count Fibonacci numbers, measure latency.
+ int a = 0;
+ int b = 1;
+ int c;
+ tr.mTimings = new long[NUM_REQUESTS];
+ for (int j = 0; j < NUM_REQUESTS; j++) {
+ long start = System.nanoTime();
+ c = ts.addInteger(a, b);
+ tr.mTimings[j] = System.nanoTime() - start;
+ a = b;
+ b = c;
+ }
+ });
+ testResults.assertNoException();
+ assertThat(testResults.mAddInteger).isEqualTo(579);
+ for (long duration : testResults.mTimings) {
+ requestLatencies.add((double) duration / NANO_TO_MICRO);
+ }
}
- reportMetrics(requestLatencies, "vsock/rpcbinder/request_latency", "ms");
+ reportMetrics(requestLatencies, "latency/rpcbinder", "us");
}
- private static class VsockRpcBinderLatencyListener
- implements BenchmarkVmListener.InnerListener {
- private static final int NUM_REQUESTS = 10000;
- private static final int NUM_WARMUP_REQUESTS = 10;
+ @Test
+ public void testVsockLatency() throws Exception {
+ final int NUM_WARMUPS = 10;
+ final int NUM_REQUESTS = 10_000;
- private final List<Double> mResults;
+ VirtualMachineConfig config =
+ newVmConfigBuilder()
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+ .setDebugLevel(DEBUG_LEVEL_NONE)
+ .build();
- VsockRpcBinderLatencyListener(List<Double> results) {
- mResults = results;
- }
+ List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS);
+ for (int i = 0; i < IO_TEST_TRIAL_COUNT; ++i) {
+ VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_latency" + i, config);
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (ts, tr) -> {
+ ts.runEchoReverseServer();
+ ParcelFileDescriptor pfd =
+ vm.connectVsock(ITestService.ECHO_REVERSE_PORT);
+ try (InputStream input = new AutoCloseInputStream(pfd);
+ OutputStream output = new AutoCloseOutputStream(pfd)) {
+ BufferedReader reader =
+ new BufferedReader(new InputStreamReader(input));
+ Writer writer = new OutputStreamWriter(output);
- @Override
- public void onPayloadReady(VirtualMachine vm, IBenchmarkService benchmarkService)
- throws RemoteException {
- // Warm up a few times.
- Random rand = new Random();
- for (int i = 0; i < NUM_WARMUP_REQUESTS; i++) {
- int a = rand.nextInt();
- int b = rand.nextInt();
- int c = benchmarkService.add(a, b);
- assertThat(c).isEqualTo(a + b);
- }
+ // Correctness check.
+ writer.write("hello\n");
+ writer.flush();
+ tr.mFileContent = reader.readLine().trim();
- // Use the VM to compute Fibonnacci numbers, save timestamps between requests.
- int a = 0;
- int b = 1;
- int c;
- long timestamps[] = new long[NUM_REQUESTS + 1];
- for (int i = 0; i < NUM_REQUESTS; i++) {
- timestamps[i] = System.nanoTime();
- c = benchmarkService.add(a, b);
- a = b;
- b = c;
- }
- timestamps[NUM_REQUESTS] = System.nanoTime();
+ // Warmup.
+ for (int j = 0; j < NUM_WARMUPS; ++j) {
+ String text = "test" + j + "\n";
+ writer.write(text);
+ writer.flush();
+ reader.readLine();
+ }
- // Log individual request latencies.
- for (int i = 0; i < NUM_REQUESTS; i++) {
- long diff = timestamps[i + 1] - timestamps[i];
- mResults.add((double) diff / NANO_TO_MILLI);
+ // Measured requests.
+ tr.mTimings = new long[NUM_REQUESTS];
+ for (int j = 0; j < NUM_REQUESTS; j++) {
+ String text = "test" + j + "\n";
+ long start = System.nanoTime();
+ writer.write(text);
+ writer.flush();
+ reader.readLine();
+ tr.mTimings[j] = System.nanoTime() - start;
+ }
+ }
+ });
+ testResults.assertNoException();
+ assertThat(testResults.mFileContent).isEqualTo("olleh");
+ for (long duration : testResults.mTimings) {
+ requestLatencies.add((double) duration / NANO_TO_MICRO);
}
}
+ reportMetrics(requestLatencies, "latency/vsock", "us");
}
}
diff --git a/tests/benchmark/src/native/benchmarkbinary.cpp b/tests/benchmark/src/native/benchmarkbinary.cpp
index 022698f..6cfc71d 100644
--- a/tests/benchmark/src/native/benchmarkbinary.cpp
+++ b/tests/benchmark/src/native/benchmarkbinary.cpp
@@ -96,11 +96,6 @@
return resultStatus(res);
}
- ndk::ScopedAStatus add(int32_t a, int32_t b, int32_t* out) override {
- *out = a + b;
- return ndk::ScopedAStatus::ok();
- }
-
private:
/**
* Measures the read rate for reading the given file.
diff --git a/tests/helper/Android.bp b/tests/helper/Android.bp
index 61c5dcd..c9eafad 100644
--- a/tests/helper/Android.bp
+++ b/tests/helper/Android.bp
@@ -15,6 +15,7 @@
static_libs: [
"androidx.test.runner",
"androidx.test.ext.junit",
+ "com.android.microdroid.testservice-java",
"MicrodroidTestHelper",
"truth-prebuilt",
],
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
index ba82c38..8a63578 100644
--- a/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
+++ b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
@@ -20,12 +20,14 @@
/** 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 {
String getProperty(String key) throws Exception;
}
private static final String KEY_VENDOR_DEVICE = "ro.product.vendor.device";
+ private static final String KEY_BOARD_PLATFORM = "ro.board.platform";
private static final String KEY_BUILD_TYPE = "ro.build.type";
private static final String KEY_METRICS_TAG = "debug.hypervisor.metrics_tag";
@@ -51,6 +53,11 @@
return vendorDeviceName != null && vendorDeviceName.startsWith(CUTTLEFISH_DEVICE_PREFIX);
}
+ public boolean isGs101() {
+ String platform = getProperty(KEY_BOARD_PLATFORM);
+ return "gs101".equals(platform);
+ }
+
/**
* @return whether the device is user build.
*/
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java b/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java
index b6bc479..42eb6a1 100644
--- a/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java
+++ b/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java
@@ -16,6 +16,8 @@
package com.android.microdroid.test.common;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -41,29 +43,42 @@
*/
public Map<String, Double> computeStats(List<? extends Number> metrics, String name,
String unit) {
+ List<Double> values = new ArrayList<>(metrics.size());
+ for (Number metric : metrics) {
+ values.add(metric.doubleValue());
+ }
+ Collections.sort(values);
+
double sum = 0;
double min = Double.MAX_VALUE;
double max = Double.MIN_VALUE;
- for (Number metric : metrics) {
- double d = metric.doubleValue();
+ for (Double d : values) {
sum += d;
if (min > d) min = d;
if (max < d) max = d;
}
- double avg = sum / metrics.size();
+ double avg = sum / values.size();
double sqSum = 0;
- for (Number metric : metrics) {
- double d = metric.doubleValue();
+ for (Double d : values) {
sqSum += (d - avg) * (d - avg);
}
- double stdDev = Math.sqrt(sqSum / (metrics.size() - 1));
-
+ double stdDev = Math.sqrt(sqSum / (values.size() - 1));
+ double median = Double.MIN_VALUE;
+ if (values.size() > 0) {
+ int rank = values.size() / 2;
+ if (values.size() % 2 == 0) {
+ median = (values.get(rank - 1) + values.get(rank)) / 2;
+ } else {
+ median = values.get(rank);
+ }
+ }
Map<String, Double> stats = new HashMap<String, Double>();
String prefix = mPrefix + name;
stats.put(prefix + "_min_" + unit, min);
stats.put(prefix + "_max_" + unit, max);
stats.put(prefix + "_average_" + unit, avg);
stats.put(prefix + "_stdev_" + unit, stdDev);
+ stats.put(prefix + "_median_" + unit, median);
return stats;
}
}
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 9ec36b3..bff16a2 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
@@ -17,6 +17,7 @@
import static android.content.pm.PackageManager.FEATURE_VIRTUALIZATION_FRAMEWORK;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import android.app.Instrumentation;
@@ -38,6 +39,7 @@
import com.android.microdroid.test.common.DeviceProperties;
import com.android.microdroid.test.common.MetricsProcessor;
+import com.android.microdroid.testservice.ITestService;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
@@ -51,15 +53,27 @@
import java.util.concurrent.TimeUnit;
public abstract class MicrodroidDeviceTestBase {
+ private static final String TAG = "MicrodroidDeviceTestBase";
private final String MAX_PERFORMANCE_TASK_PROFILE = "CPUSET_SP_TOP_APP";
public static boolean isCuttlefish() {
- return DeviceProperties.create(SystemProperties::get).isCuttlefish();
+ return getDeviceProperties().isCuttlefish();
+ }
+
+ public static boolean isGs101() {
+ return getDeviceProperties().isGs101();
+ }
+
+ public static boolean isUserBuild() {
+ return getDeviceProperties().isUserBuild();
}
public static String getMetricPrefix() {
- return MetricsProcessor.getMetricPrefix(
- DeviceProperties.create(SystemProperties::get).getMetricsTag());
+ return MetricsProcessor.getMetricPrefix(getDeviceProperties().getMetricsTag());
+ }
+
+ private static DeviceProperties getDeviceProperties() {
+ return DeviceProperties.create(SystemProperties::get);
}
protected final void grantPermission(String permission) {
@@ -80,13 +94,22 @@
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
UiAutomation uiAutomation = instrumentation.getUiAutomation();
String cmd = "settaskprofile " + Os.gettid() + " " + MAX_PERFORMANCE_TASK_PROFILE;
- String out = runInShell("MicrodroidDeviceTestBase", uiAutomation, cmd).trim();
+ String out = runInShell(TAG, uiAutomation, cmd).trim();
String expect = "Profile " + MAX_PERFORMANCE_TASK_PROFILE + " is applied successfully!";
if (!expect.equals(out)) {
throw new IOException("Could not apply max performance task profile: " + out);
}
}
+ public final boolean getDebugPolicyBoolean(String debugPolicy) throws IOException {
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ UiAutomation uiAutomation = instrumentation.getUiAutomation();
+ String debugPolicyFilePath = "/proc/device-tree" + debugPolicy;
+ String cmd = "su root xxd -p " + debugPolicyFilePath;
+ String dp = runInShell(TAG, uiAutomation, cmd).trim();
+ return "00000001".equals(dp);
+ }
+
private Context mCtx;
private boolean mProtectedVm;
@@ -428,4 +451,106 @@
throw new RuntimeException("Failed to run the command.");
}
}
+
+ protected static class TestResults {
+ public Exception mException;
+ public Integer mAddInteger;
+ public String mAppRunProp;
+ public String mSublibRunProp;
+ public String mExtraApkTestProp;
+ public String mApkContentsPath;
+ public String mEncryptedStoragePath;
+ public String[] mEffectiveCapabilities;
+ public String mFileContent;
+ public byte[] mBcc;
+ public long[] mTimings;
+ public int mFileMode;
+ public int mMountFlags;
+
+ public void assertNoException() {
+ if (mException != null) {
+ // Rethrow, wrapped in a new exception, so we get stack traces of the original
+ // failure as well as the body of the test.
+ throw new RuntimeException(mException);
+ }
+ }
+ }
+
+ protected TestResults runVmTestService(
+ String logTag, VirtualMachine vm, RunTestsAgainstTestService testsToRun)
+ throws Exception {
+ CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
+ CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
+ CompletableFuture<Boolean> payloadFinished = new CompletableFuture<>();
+ TestResults testResults = new TestResults();
+ VmEventListener listener =
+ new VmEventListener() {
+ ITestService mTestService = null;
+
+ private void initializeTestService(VirtualMachine vm) {
+ try {
+ mTestService =
+ ITestService.Stub.asInterface(
+ vm.connectToVsockServer(ITestService.SERVICE_PORT));
+ // Make sure linkToDeath works, and include it in the log in case it's
+ // helpful.
+ mTestService
+ .asBinder()
+ .linkToDeath(
+ () -> Log.i(logTag, "ITestService binder died"), 0);
+ } catch (Exception e) {
+ testResults.mException = e;
+ }
+ }
+
+ private void testVMService(VirtualMachine vm) {
+ try {
+ if (mTestService == null) initializeTestService(vm);
+ testsToRun.runTests(mTestService, testResults);
+ } catch (Exception e) {
+ testResults.mException = e;
+ }
+ }
+
+ private void quitVMService() {
+ try {
+ mTestService.quit();
+ } catch (Exception e) {
+ testResults.mException = e;
+ }
+ }
+
+ @Override
+ public void onPayloadReady(VirtualMachine vm) {
+ Log.i(logTag, "onPayloadReady");
+ payloadReady.complete(true);
+ testVMService(vm);
+ quitVMService();
+ }
+
+ @Override
+ public void onPayloadStarted(VirtualMachine vm) {
+ Log.i(logTag, "onPayloadStarted");
+ payloadStarted.complete(true);
+ }
+
+ @Override
+ public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+ Log.i(logTag, "onPayloadFinished: " + exitCode);
+ payloadFinished.complete(true);
+ forceStop(vm);
+ }
+ };
+
+ listener.runToFinish(logTag, vm);
+ assertThat(payloadStarted.getNow(false)).isTrue();
+ assertThat(payloadReady.getNow(false)).isTrue();
+ assertThat(payloadFinished.getNow(false)).isTrue();
+ return testResults;
+ }
+
+ @FunctionalInterface
+ protected interface RunTestsAgainstTestService {
+ void runTests(ITestService testService, TestResults testResults) throws Exception;
+ }
}
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index f1e5054..d217c00 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -22,6 +22,20 @@
out: ["avf_debug_policy_without_ramdump.dtbo"],
}
+genrule {
+ name: "test_avf_debug_policy_with_console_output",
+ defaults: ["test_avf_debug_policy_overlay"],
+ srcs: ["assets/avf_debug_policy_with_console_output.dts"],
+ out: ["avf_debug_policy_with_console_output.dtbo"],
+}
+
+genrule {
+ name: "test_avf_debug_policy_without_console_output",
+ defaults: ["test_avf_debug_policy_overlay"],
+ srcs: ["assets/avf_debug_policy_without_console_output.dts"],
+ out: ["avf_debug_policy_without_console_output.dtbo"],
+}
+
java_test_host {
name: "MicrodroidHostTestCases",
srcs: ["java/**/*.java"],
@@ -48,6 +62,8 @@
":pvmfw_test",
":test_avf_debug_policy_with_ramdump",
":test_avf_debug_policy_without_ramdump",
+ ":test_avf_debug_policy_with_console_output",
+ ":test_avf_debug_policy_without_console_output",
"assets/bcc.dat",
],
data_native_bins: [
diff --git a/tests/hostside/assets/avf_debug_policy_with_console_output.dts b/tests/hostside/assets/avf_debug_policy_with_console_output.dts
new file mode 100644
index 0000000..8cf19d6
--- /dev/null
+++ b/tests/hostside/assets/avf_debug_policy_with_console_output.dts
@@ -0,0 +1,18 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+ fragment@avf {
+ target-path = "/";
+
+ __overlay__ {
+ avf {
+ guest {
+ common {
+ log = <1>;
+ };
+ };
+ };
+ };
+ };
+};
\ No newline at end of file
diff --git a/tests/hostside/assets/avf_debug_policy_without_console_output.dts b/tests/hostside/assets/avf_debug_policy_without_console_output.dts
new file mode 100644
index 0000000..da6400c
--- /dev/null
+++ b/tests/hostside/assets/avf_debug_policy_without_console_output.dts
@@ -0,0 +1,18 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+ fragment@avf {
+ target-path = "/";
+
+ __overlay__ {
+ avf {
+ guest {
+ common {
+ log = <0>;
+ };
+ };
+ };
+ };
+ };
+};
\ No newline at end of file
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 0be6a62..1fa0afe 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -827,6 +827,7 @@
assumeTrue(
"Protected VMs are not supported",
getAndroidDevice().supportsMicrodroid(/*protectedVm=*/ true));
+ assumeTrue("Test requires adb unroot", getDevice().disableAdbRoot());
CommandRunner android = new CommandRunner(getDevice());
// Pull etc/microdroid.json
diff --git a/tests/hostside/java/com/android/microdroid/test/PvmfwDebugPolicyHostTests.java b/tests/hostside/java/com/android/microdroid/test/PvmfwDebugPolicyHostTests.java
index bda52dd..755613a 100644
--- a/tests/hostside/java/com/android/microdroid/test/PvmfwDebugPolicyHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/PvmfwDebugPolicyHostTests.java
@@ -19,8 +19,10 @@
import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assume.assumeTrue;
+import static org.junit.Assume.assumeFalse;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -32,6 +34,7 @@
import com.android.tradefed.device.TestDevice;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.FileUtil;
import org.junit.After;
@@ -41,6 +44,7 @@
import java.io.File;
import java.util.Objects;
+import java.util.concurrent.TimeUnit;
import java.io.FileNotFoundException;
/** Tests debug policy of pvmfw.bin with custom debug policy */
@@ -50,13 +54,15 @@
@NonNull private static final String BCC_FILE_NAME = "bcc.dat";
@NonNull private static final String PACKAGE_FILE_NAME = "MicrodroidTestApp.apk";
@NonNull private static final String PACKAGE_NAME = "com.android.microdroid.test";
- @NonNull private static final String MICRODROID_DEBUG_LEVEL = "full";
+ @NonNull private static final String MICRODROID_DEBUG_FULL = "full";
@NonNull private static final String MICRODROID_CONFIG_PATH = "assets/vm_config_apex.json";
- private static final int BOOT_COMPLETE_TIMEOUT = 30000; // 30 seconds
+ @NonNull private static final String MICRODROID_LOG_PATH = TEST_ROOT + "log.txt";
+ private static final int BOOT_COMPLETE_TIMEOUT_MS = 30000; // 30 seconds
+ private static final int CONSOLE_OUTPUT_WAIT_MS = 5000; // 5 seconds
@NonNull private static final String CUSTOM_PVMFW_FILE_PREFIX = "pvmfw";
@NonNull private static final String CUSTOM_PVMFW_FILE_SUFFIX = ".bin";
- @NonNull private static final String CUSTOM_PVMFW_IMG_PATH = TEST_ROOT + "/" + PVMFW_FILE_NAME;
+ @NonNull private static final String CUSTOM_PVMFW_IMG_PATH = TEST_ROOT + PVMFW_FILE_NAME;
@NonNull private static final String CUSTOM_PVMFW_IMG_PATH_PROP = "hypervisor.pvmfw.path";
@NonNull private static final String MICRODROID_CMDLINE_PATH = "/proc/cmdline";
@@ -86,6 +92,7 @@
assumeTrue(
"Skip if protected VMs are not supported",
mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true));
+ assumeFalse("Test requires setprop for using custom pvmfw and adb root", isUserBuild());
mAndroidDevice.enableAdbRoot();
@@ -162,6 +169,30 @@
.isEqualTo(HEX_STRING_ZERO);
}
+ @Test
+ public void testConsoleOutput() throws Exception {
+ Pvmfw pvmfw = createPvmfw("avf_debug_policy_with_console_output.dtbo");
+ pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+ CommandResult result = tryLaunchProtectedNonDebuggableVm();
+
+ assertWithMessage("Microdroid's console message should have been enabled")
+ .that(hasConsoleOutput(result))
+ .isTrue();
+ }
+
+ @Test
+ public void testNoConsoleOutput() throws Exception {
+ Pvmfw pvmfw = createPvmfw("avf_debug_policy_without_console_output.dtbo");
+ pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+ CommandResult result = tryLaunchProtectedNonDebuggableVm();
+
+ assertWithMessage("Microdroid's console message shouldn't have been disabled")
+ .that(hasConsoleOutput(result))
+ .isFalse();
+ }
+
@NonNull
private String readMicrodroidFileAsString(@NonNull String path)
throws DeviceNotAvailableException {
@@ -184,18 +215,50 @@
.build();
}
+ @NonNull
+ private boolean hasConsoleOutput(CommandResult result) throws DeviceNotAvailableException {
+ return result.getStdout().contains("Run /init as init process");
+ }
+
private ITestDevice launchProtectedVmAndWaitForBootCompleted()
throws DeviceNotAvailableException {
mMicrodroidDevice =
MicrodroidBuilder.fromDevicePath(
getPathForPackage(PACKAGE_NAME), MICRODROID_CONFIG_PATH)
- .debugLevel(MICRODROID_DEBUG_LEVEL)
+ .debugLevel(MICRODROID_DEBUG_FULL)
.protectedVm(/* protectedVm= */ true)
.addBootFile(mCustomPvmfwBinFileOnHost, PVMFW_FILE_NAME)
.build(mAndroidDevice);
- assertThat(mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT)).isTrue();
+ assertThat(mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT_MS)).isTrue();
assertThat(mMicrodroidDevice.enableAdbRoot()).isTrue();
-
return mMicrodroidDevice;
}
+
+ // Try to launch protected non-debuggable VM for a while and quit.
+ // Non-debuggable VM doesn't enable adb, so there's no ITestDevice instance of it.
+ private CommandResult tryLaunchProtectedNonDebuggableVm() throws DeviceNotAvailableException {
+ // Can't use MicrodroidBuilder because it expects adb connection
+ // but non-debuggable VM doesn't enable adb.
+ CommandRunner runner = new CommandRunner(mAndroidDevice);
+ runner.run("mkdir", "-p", TEST_ROOT);
+ mAndroidDevice.pushFile(mCustomPvmfwBinFileOnHost, TEST_ROOT + PVMFW_FILE_NAME);
+
+ // This will fail because app wouldn't finish itself.
+ // But let's run the app once and get logs.
+ String command =
+ String.join(
+ " ",
+ "/apex/com.android.virt/bin/vm",
+ "run-app",
+ "--log",
+ MICRODROID_LOG_PATH,
+ "--protected",
+ getPathForPackage(PACKAGE_NAME),
+ TEST_ROOT + "idsig",
+ TEST_ROOT + "instance.img",
+ "--config-path",
+ MICRODROID_CONFIG_PATH);
+ return mAndroidDevice.executeShellV2Command(
+ command, CONSOLE_OUTPUT_WAIT_MS, TimeUnit.MILLISECONDS, /* retryAttempts= */ 0);
+ }
}
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index bafab53..fe8f5c9 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -31,6 +31,7 @@
"cbor-java",
"truth-prebuilt",
"compatibility-common-util-devicesidelib",
+ "measure_io_as_jar",
],
jni_libs: [
"MicrodroidTestNativeLib",
@@ -62,6 +63,7 @@
static_libs: [
"com.android.microdroid.testservice-ndk",
"libbase",
+ "libfstab",
"libfsverity_digests_proto_cc",
"liblog",
"libprotobuf-cpp-lite-ndk",
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 a66f9c3..6e4694c 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -28,6 +28,7 @@
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static org.junit.Assume.assumeFalse;
import static org.junit.Assert.assertThrows;
import com.google.common.base.Strings;
@@ -45,6 +46,7 @@
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.os.SystemProperties;
+import android.system.OsConstants;
import android.system.virtualmachine.VirtualMachine;
import android.system.virtualmachine.VirtualMachineCallback;
import android.system.virtualmachine.VirtualMachineConfig;
@@ -56,7 +58,9 @@
import com.android.compatibility.common.util.CddTest;
import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
import com.android.microdroid.test.vmshare.IVmShareTestService;
+import com.android.microdroid.testservice.IAppCallback;
import com.android.microdroid.testservice.ITestService;
+import com.android.microdroid.testservice.IVmCallback;
import org.junit.After;
import org.junit.Before;
@@ -134,7 +138,7 @@
private static final String VM_SHARE_APP_PACKAGE_NAME = "com.android.microdroid.vmshare_app";
private void createAndConnectToVmHelper(int cpuTopology) throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -147,6 +151,7 @@
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mAddInteger = ts.addInteger(123, 456);
@@ -178,7 +183,7 @@
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
public void createAndRunNoDebugVm() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
// For most of our tests we use a debug VM so failures can be diagnosed.
// But we do need non-debug VMs to work, so run one.
@@ -192,7 +197,7 @@
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
TestResults testResults =
- runVmTestService(vm, (ts, tr) -> tr.mAddInteger = ts.addInteger(37, 73));
+ runVmTestService(TAG, vm, (ts, tr) -> tr.mAddInteger = ts.addInteger(37, 73));
testResults.assertNoException();
assertThat(testResults.mAddInteger).isEqualTo(37 + 73);
}
@@ -205,7 +210,7 @@
"9.17/C-1-4",
})
public void createVmRequiresPermission() {
- assumeSupportedKernel();
+ assumeSupportedDevice();
revokePermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
@@ -226,7 +231,7 @@
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void autoCloseVm() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -311,7 +316,7 @@
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void vmLifecycleChecks() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -360,7 +365,7 @@
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void connectVsock() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -375,6 +380,7 @@
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(service, results) -> {
service.runEchoReverseServer();
@@ -397,6 +403,59 @@
@Test
@CddTest(requirements = {"9.17/C-1-1"})
+ public void binderCallbacksWork() throws Exception {
+ assumeSupportedDevice();
+
+ VirtualMachineConfig config =
+ newVmConfigBuilder()
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+ .setMemoryBytes(minMemoryRequired())
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .build();
+ VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
+
+ String request = "Hello";
+ CompletableFuture<String> response = new CompletableFuture<>();
+
+ IAppCallback appCallback =
+ new IAppCallback.Stub() {
+ @Override
+ public void setVmCallback(IVmCallback vmCallback) {
+ // Do this on a separate thread to simulate an asynchronous trigger,
+ // and to make sure it doesn't happen in the context of an inbound binder
+ // call.
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ vmCallback.echoMessage(request);
+ } catch (Exception e) {
+ response.completeExceptionally(e);
+ }
+ }
+ }.start();
+ }
+
+ @Override
+ public void onEchoRequestReceived(String message) {
+ response.complete(message);
+ }
+ };
+
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (service, results) -> {
+ service.requestCallback(appCallback);
+ response.get(10, TimeUnit.SECONDS);
+ });
+ testResults.assertNoException();
+ assertThat(response.getNow("no response")).isEqualTo("Received: " + request);
+ }
+
+ @Test
+ @CddTest(requirements = {"9.17/C-1-1"})
public void vmConfigGetAndSetTests() {
// Minimal has as little as specified as possible; everything that can be is defaulted.
VirtualMachineConfig.Builder minimalBuilder = newVmConfigBuilder();
@@ -577,7 +636,7 @@
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void vmmGetAndCreate() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -675,7 +734,7 @@
"9.17/C-1-4",
})
public void createVmWithConfigRequiresPermission() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -687,7 +746,8 @@
forceCreateNewVirtualMachine("test_vm_config_requires_permission", config);
SecurityException e =
- assertThrows(SecurityException.class, () -> runVmTestService(vm, (ts, tr) -> {}));
+ assertThrows(
+ SecurityException.class, () -> runVmTestService(TAG, vm, (ts, tr) -> {}));
assertThat(e).hasMessageThat()
.contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission");
}
@@ -697,7 +757,7 @@
"9.17/C-1-1",
})
public void deleteVm() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -725,7 +785,7 @@
"9.17/C-1-1",
})
public void deleteVmFiles() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -758,7 +818,7 @@
"9.17/C-1-1",
})
public void validApkPathIsAccepted() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -772,6 +832,7 @@
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mApkContentsPath = ts.getApkContentsPath();
@@ -794,7 +855,7 @@
"9.17/C-2-1"
})
public void extraApk() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig config =
@@ -807,6 +868,7 @@
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mExtraApkTestProp =
@@ -861,7 +923,7 @@
}
private void changeDebugLevel(int fromLevel, int toLevel) throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig.Builder builder =
newVmConfigBuilder()
@@ -932,7 +994,7 @@
"9.17/C-2-7"
})
public void instancesOfSameVmHaveDifferentCdis() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig normalConfig =
@@ -958,7 +1020,7 @@
"9.17/C-2-7"
})
public void sameInstanceKeepsSameCdis() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
assume().withMessage("Skip on CF. Too Slow. b/257270529").that(isCuttlefish()).isFalse();
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
@@ -983,7 +1045,7 @@
"9.17/C-2-7"
})
public void bccIsSuperficiallyWellFormed() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig normalConfig =
@@ -994,6 +1056,7 @@
VirtualMachine vm = forceCreateNewVirtualMachine("bcc_vm", normalConfig);
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(service, results) -> {
results.mBcc = service.getBcc();
@@ -1021,7 +1084,7 @@
"9.17/C-1-2"
})
public void accessToCdisIsRestricted() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -1221,7 +1284,7 @@
@Test
public void importedVmAndOriginalVmHaveTheSameCdi() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
// Arrange
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig config =
@@ -1279,6 +1342,7 @@
// Run something to make the instance.img different with the initialized one.
TestResults origTestResults =
runVmTestService(
+ TAG,
vmOrig,
(ts, tr) -> {
tr.mAddInteger = ts.addInteger(123, 456);
@@ -1305,6 +1369,7 @@
assertThat(vmImport).isEqualTo(vmm.get(vmNameImport));
TestResults testResults =
runVmTestService(
+ TAG,
vmImport,
(ts, tr) -> {
tr.mAddInteger = ts.addInteger(123, 456);
@@ -1318,7 +1383,7 @@
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void encryptedStorageAvailable() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -1331,6 +1396,7 @@
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
@@ -1341,7 +1407,7 @@
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void encryptedStorageIsInaccessibleToDifferentVm() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -1355,6 +1421,7 @@
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
ts.writeToFile(
@@ -1401,7 +1468,7 @@
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
public void microdroidLauncherHasEmptyCapabilities() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
final VirtualMachineConfig vmConfig =
newVmConfigBuilder()
@@ -1413,6 +1480,7 @@
final TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mEffectiveCapabilities = ts.getEffectiveCapabilities();
@@ -1425,7 +1493,7 @@
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void encryptedStorageIsPersistent() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -1437,6 +1505,7 @@
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_a", config);
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
ts.writeToFile(
@@ -1449,6 +1518,7 @@
// stopped the VM
testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/test_file");
@@ -1460,7 +1530,7 @@
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
public void canReadFileFromAssets_debugFull() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -1472,6 +1542,7 @@
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(testService, ts) -> {
ts.mFileContent = testService.readFromFile("/mnt/apk/assets/file.txt");
@@ -1483,7 +1554,7 @@
@Test
public void outputShouldBeExplicitlyCaptured() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
final VirtualMachineConfig vmConfig =
new VirtualMachineConfig.Builder(getContext())
@@ -1504,6 +1575,22 @@
}
}
+ private boolean isConsoleOutputEnabledByDebugPolicy() {
+ if (isUserBuild()) {
+ Log.i(
+ TAG,
+ "Debug policy is inaccessible in user build. Assumes that console output is"
+ + " disabled");
+ return false;
+ }
+ try {
+ return getDebugPolicyBoolean("/avf/guest/common/log");
+ } catch (IOException e) {
+ Log.w(TAG, "Fail to read debug policy. Assumes false", e);
+ return false;
+ }
+ }
+
private boolean checkVmOutputIsRedirectedToLogcat(boolean debuggable) throws Exception {
String time =
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
@@ -1516,7 +1603,7 @@
.build();
final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_logcat", vmConfig);
- runVmTestService(vm, (service, results) -> {});
+ runVmTestService(TAG, vm, (service, results) -> {});
// only check logs printed after this test
Process logcatProcess =
@@ -1536,21 +1623,27 @@
@Test
public void outputIsRedirectedToLogcatIfNotCaptured() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
+ assumeFalse(
+ "Debug policy would turn on console output. Perhaps userdebug build?",
+ isConsoleOutputEnabledByDebugPolicy());
assertThat(checkVmOutputIsRedirectedToLogcat(true)).isTrue();
}
@Test
public void outputIsNotRedirectedToLogcatIfNotDebuggable() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
+ assumeFalse(
+ "Debug policy would turn on console output. Perhaps userdebug build?",
+ isConsoleOutputEnabledByDebugPolicy());
assertThat(checkVmOutputIsRedirectedToLogcat(false)).isFalse();
}
@Test
public void testStartVmWithPayloadOfAnotherApp() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
Context ctx = getContext();
Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
@@ -1565,6 +1658,7 @@
try (VirtualMachine vm = forceCreateNewVirtualMachine("vm_from_another_app", config)) {
TestResults results =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mAddInteger = ts.addInteger(101, 303);
@@ -1577,7 +1671,7 @@
@Test
public void testVmDescriptorParcelUnparcel_noTrustedStorage() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -1587,7 +1681,7 @@
VirtualMachine originalVm = forceCreateNewVirtualMachine("original_vm", config);
// Just start & stop the VM.
- runVmTestService(originalVm, (ts, tr) -> {});
+ runVmTestService(TAG, originalVm, (ts, tr) -> {});
// Now create the descriptor and manually parcel & unparcel it.
VirtualMachineDescriptor vmDescriptor = toParcelFromParcel(originalVm.toDescriptor());
@@ -1606,12 +1700,12 @@
"instance.img", "original_vm", "import_vm_from_unparceled");
// Check that we can start and stop imported vm as well
- runVmTestService(importVm, (ts, tr) -> {});
+ runVmTestService(TAG, importVm, (ts, tr) -> {});
}
@Test
public void testVmDescriptorParcelUnparcel_withTrustedStorage() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -1625,6 +1719,7 @@
{
TestResults testResults =
runVmTestService(
+ TAG,
originalVm,
(ts, tr) -> {
ts.writeToFile("not a secret!", "/mnt/encryptedstore/secret.txt");
@@ -1652,6 +1747,7 @@
TestResults testResults =
runVmTestService(
+ TAG,
importVm,
(ts, tr) -> {
tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/secret.txt");
@@ -1663,7 +1759,7 @@
@Test
public void testShareVmWithAnotherApp() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
Context ctx = getContext();
Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
@@ -1677,7 +1773,7 @@
VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config);
// Just start & stop the VM.
- runVmTestService(vm, (ts, tr) -> {});
+ runVmTestService(TAG, vm, (ts, tr) -> {});
// Get a descriptor that we will share with another app (VM_SHARE_APP_PACKAGE_NAME)
VirtualMachineDescriptor vmDesc = vm.toDescriptor();
@@ -1686,6 +1782,7 @@
new ComponentName(
VM_SHARE_APP_PACKAGE_NAME,
"com.android.microdroid.test.sharevm.VmShareServiceImpl"));
+ serviceIntent.setAction("com.android.microdroid.test.sharevm.VmShareService");
VmShareServiceConnection connection = new VmShareServiceConnection();
boolean ret = ctx.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE);
@@ -1710,7 +1807,7 @@
@Test
public void testShareVmWithAnotherApp_encryptedStorage() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
Context ctx = getContext();
Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
@@ -1726,6 +1823,7 @@
VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config);
// Just start & stop the VM.
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
ts.writeToFile(EXAMPLE_STRING, "/mnt/encryptedstore/private.key");
@@ -1738,6 +1836,7 @@
new ComponentName(
VM_SHARE_APP_PACKAGE_NAME,
"com.android.microdroid.test.sharevm.VmShareServiceImpl"));
+ serviceIntent.setAction("com.android.microdroid.test.sharevm.VmShareService");
VmShareServiceConnection connection = new VmShareServiceConnection();
boolean ret = ctx.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE);
@@ -1760,6 +1859,98 @@
}
}
+ @Test
+ @CddTest(requirements = {"9.17/C-1-5"})
+ public void testFileUnderBinHasExecutePermission() throws Exception {
+ assumeSupportedDevice();
+
+ VirtualMachineConfig vmConfig =
+ newVmConfigBuilder()
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+ .setMemoryBytes(minMemoryRequired())
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .build();
+ VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_perms", vmConfig);
+
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (ts, tr) -> {
+ tr.mFileMode = ts.getFilePermissions("/mnt/apk/bin/measure_io");
+ });
+
+ testResults.assertNoException();
+ int allPermissionsMask =
+ OsConstants.S_IRUSR
+ | OsConstants.S_IWUSR
+ | OsConstants.S_IXUSR
+ | OsConstants.S_IRGRP
+ | OsConstants.S_IWGRP
+ | OsConstants.S_IXGRP
+ | OsConstants.S_IROTH
+ | OsConstants.S_IWOTH
+ | OsConstants.S_IXOTH;
+ assertThat(testResults.mFileMode & allPermissionsMask)
+ .isEqualTo(OsConstants.S_IRUSR | OsConstants.S_IXUSR);
+ }
+
+ // Taken from bionic/libs/kernel/uapi/linux/mounth.h.
+ private static final int MS_NOEXEC = 8;
+
+ @Test
+ @CddTest(requirements = {"9.17/C-1-5"})
+ public void dataIsMountedWithNoExec() throws Exception {
+ assumeSupportedDevice();
+
+ VirtualMachineConfig vmConfig =
+ newVmConfigBuilder()
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .build();
+ VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_data_mount", vmConfig);
+
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (ts, tr) -> {
+ tr.mMountFlags = ts.getMountFlags("/data");
+ });
+
+ assertThat(testResults.mException).isNull();
+ assertWithMessage("/data should be mounted with MS_NOEXEC")
+ .that(testResults.mMountFlags & MS_NOEXEC)
+ .isEqualTo(MS_NOEXEC);
+ }
+
+ @Test
+ @CddTest(requirements = {"9.17/C-1-5"})
+ public void encryptedStoreIsMountedWithNoExec() throws Exception {
+ assumeSupportedDevice();
+
+ VirtualMachineConfig vmConfig =
+ newVmConfigBuilder()
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .setEncryptedStorageBytes(4_000_000)
+ .build();
+ VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_encstore_no_exec", vmConfig);
+
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (ts, tr) -> {
+ tr.mMountFlags = ts.getMountFlags("/mnt/encryptedstore");
+ });
+
+ assertThat(testResults.mException).isNull();
+ assertWithMessage("/mnt/encryptedstore should be mounted with MS_NOEXEC")
+ .that(testResults.mMountFlags & MS_NOEXEC)
+ .isEqualTo(MS_NOEXEC);
+ }
+
private static class VmShareServiceConnection implements ServiceConnection {
private final CountDownLatch mLatch = new CountDownLatch(1);
@@ -1829,108 +2020,16 @@
return 0;
}
- private void assumeSupportedKernel() {
+ private void assumeSupportedDevice() {
assume()
.withMessage("Skip on 5.4 kernel. b/218303240")
.that(KERNEL_VERSION)
.isNotEqualTo("5.4");
- }
- static class TestResults {
-
- Exception mException;
- Integer mAddInteger;
- String mAppRunProp;
- String mSublibRunProp;
- String mExtraApkTestProp;
- String mApkContentsPath;
- String mEncryptedStoragePath;
- String[] mEffectiveCapabilities;
- String mFileContent;
- byte[] mBcc;
-
- void assertNoException() {
- if (mException != null) {
- // Rethrow, wrapped in a new exception, so we get stack traces of the original
- // failure as well as the body of the test.
- throw new RuntimeException(mException);
- }
+ if (isProtectedVm()) {
+ assume().withMessage("Protected VMs not supported on gs101 devices. b/270841564")
+ .that(isGs101())
+ .isFalse();
}
}
-
- private TestResults runVmTestService(VirtualMachine vm, RunTestsAgainstTestService testsToRun)
- throws Exception {
- CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
- CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
- CompletableFuture<Boolean> payloadFinished = new CompletableFuture<>();
- TestResults testResults = new TestResults();
- VmEventListener listener =
- new VmEventListener() {
- ITestService mTestService = null;
-
- private void initializeTestService(VirtualMachine vm) {
- try {
- mTestService =
- ITestService.Stub.asInterface(
- vm.connectToVsockServer(ITestService.SERVICE_PORT));
- // Make sure linkToDeath works, and include it in the log in case it's
- // helpful.
- mTestService
- .asBinder()
- .linkToDeath(() -> Log.i(TAG, "ITestService binder died"), 0);
- } catch (Exception e) {
- testResults.mException = e;
- }
- }
-
- private void testVMService(VirtualMachine vm) {
- try {
- if (mTestService == null) initializeTestService(vm);
- testsToRun.runTests(mTestService, testResults);
- } catch (Exception e) {
- testResults.mException = e;
- }
- }
-
- private void quitVMService() {
- try {
- mTestService.quit();
- } catch (Exception e) {
- testResults.mException = e;
- }
- }
-
- @Override
- public void onPayloadReady(VirtualMachine vm) {
- Log.i(TAG, "onPayloadReady");
- payloadReady.complete(true);
- testVMService(vm);
- quitVMService();
- }
-
- @Override
- public void onPayloadStarted(VirtualMachine vm) {
- Log.i(TAG, "onPayloadStarted");
- payloadStarted.complete(true);
- }
-
- @Override
- public void onPayloadFinished(VirtualMachine vm, int exitCode) {
- Log.i(TAG, "onPayloadFinished: " + exitCode);
- payloadFinished.complete(true);
- forceStop(vm);
- }
- };
-
- listener.runToFinish(TAG, vm);
- assertThat(payloadStarted.getNow(false)).isTrue();
- assertThat(payloadReady.getNow(false)).isTrue();
- assertThat(payloadFinished.getNow(false)).isTrue();
- return testResults;
- }
-
- @FunctionalInterface
- interface RunTestsAgainstTestService {
- void runTests(ITestService testService, TestResults testResults) throws Exception;
- }
}
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 365ea75..d24ddfd 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -15,12 +15,15 @@
*/
#include <aidl/com/android/microdroid/testservice/BnTestService.h>
+#include <aidl/com/android/microdroid/testservice/BnVmCallback.h>
+#include <aidl/com/android/microdroid/testservice/IAppCallback.h>
#include <android-base/file.h>
#include <android-base/properties.h>
#include <android-base/result.h>
#include <android-base/scopeguard.h>
#include <android/log.h>
#include <fcntl.h>
+#include <fstab/fstab.h>
#include <fsverity_digests.pb.h>
#include <linux/vm_sockets.h>
#include <stdint.h>
@@ -40,8 +43,14 @@
using android::base::make_scope_guard;
using android::base::Result;
using android::base::unique_fd;
+using android::fs_mgr::Fstab;
+using android::fs_mgr::FstabEntry;
+using android::fs_mgr::GetEntryForMountPoint;
+using android::fs_mgr::ReadFstabFromFile;
using aidl::com::android::microdroid::testservice::BnTestService;
+using aidl::com::android::microdroid::testservice::BnVmCallback;
+using aidl::com::android::microdroid::testservice::IAppCallback;
using ndk::ScopedAStatus;
extern void testlib_sub();
@@ -84,28 +93,26 @@
return ErrnoError() << "Failed to fdopen";
}
- char* line = nullptr;
- size_t size = 0;
- if (getline(&line, &size, input) < 0) {
- return ErrnoError() << "Failed to read";
+ // Run forever, reverse one line at a time.
+ while (true) {
+ char* line = nullptr;
+ size_t size = 0;
+ if (getline(&line, &size, input) < 0) {
+ return ErrnoError() << "Failed to read";
+ }
+
+ std::string_view original = line;
+ if (!original.empty() && original.back() == '\n') {
+ original = original.substr(0, original.size() - 1);
+ }
+
+ std::string reversed(original.rbegin(), original.rend());
+ reversed += "\n";
+
+ if (write(connect_fd, reversed.data(), reversed.size()) < 0) {
+ return ErrnoError() << "Failed to write";
+ }
}
-
- if (fclose(input) != 0) {
- return ErrnoError() << "Failed to fclose";
- }
-
- std::string_view original = line;
- if (!original.empty() && original.back() == '\n') {
- original = original.substr(0, original.size() - 1);
- }
-
- std::string reversed(original.rbegin(), original.rend());
-
- if (write(connect_fd, reversed.data(), reversed.size()) < 0) {
- return ErrnoError() << "Failed to write";
- }
-
- return {};
}
Result<void> start_echo_reverse_server() {
@@ -141,7 +148,25 @@
}
Result<void> start_test_service() {
+ class VmCallbackImpl : public BnVmCallback {
+ private:
+ std::shared_ptr<IAppCallback> mAppCallback;
+
+ public:
+ explicit VmCallbackImpl(const std::shared_ptr<IAppCallback>& appCallback)
+ : mAppCallback(appCallback) {}
+
+ ScopedAStatus echoMessage(const std::string& message) override {
+ std::thread callback_thread{[=, appCallback = mAppCallback] {
+ appCallback->onEchoRequestReceived("Received: " + message);
+ }};
+ callback_thread.detach();
+ return ScopedAStatus::ok();
+ }
+ };
+
class TestService : public BnTestService {
+ public:
ScopedAStatus addInteger(int32_t a, int32_t b, int32_t* out) override {
*out = a + b;
return ScopedAStatus::ok();
@@ -223,7 +248,7 @@
return ScopedAStatus::ok();
}
- virtual ::ScopedAStatus runEchoReverseServer() override {
+ ScopedAStatus runEchoReverseServer() override {
auto result = start_echo_reverse_server();
if (result.ok()) {
return ScopedAStatus::ok();
@@ -253,6 +278,41 @@
return ScopedAStatus::ok();
}
+ ScopedAStatus getFilePermissions(const std::string& path, int32_t* out) override {
+ struct stat sb;
+ if (stat(path.c_str(), &sb) != -1) {
+ *out = sb.st_mode;
+ } else {
+ std::string msg = "stat " + path + " failed : " + std::strerror(errno);
+ return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+ msg.c_str());
+ }
+ return ScopedAStatus::ok();
+ }
+
+ ScopedAStatus getMountFlags(const std::string& mount_point, int32_t* out) override {
+ Fstab fstab;
+ if (!ReadFstabFromFile("/proc/mounts", &fstab)) {
+ return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+ "Failed to read /proc/mounts");
+ }
+ FstabEntry* entry = GetEntryForMountPoint(&fstab, mount_point);
+ if (entry == nullptr) {
+ std::string msg = mount_point + " not found in /proc/mounts";
+ return ScopedAStatus::fromExceptionCodeWithMessage(EX_SERVICE_SPECIFIC,
+ msg.c_str());
+ }
+ *out = entry->flags;
+ return ScopedAStatus::ok();
+ }
+
+ ScopedAStatus requestCallback(const std::shared_ptr<IAppCallback>& appCallback) {
+ auto vmCallback = ndk::SharedRefBase::make<VmCallbackImpl>(appCallback);
+ std::thread callback_thread{[=] { appCallback->setVmCallback(vmCallback); }};
+ callback_thread.detach();
+ return ScopedAStatus::ok();
+ }
+
ScopedAStatus quit() override { exit(0); }
};
auto testService = ndk::SharedRefBase::make<TestService>();
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
index 278e1a2..edd6bf5 100644
--- a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -29,6 +29,7 @@
import com.android.microdroid.test.vmshare.IVmShareTestService;
import com.android.microdroid.testservice.ITestService;
+import com.android.microdroid.testservice.IAppCallback;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
@@ -230,6 +231,21 @@
}
@Override
+ public int getFilePermissions(String path) throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public int getMountFlags(String path) throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public void requestCallback(IAppCallback appCallback) {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
public void quit() throws RemoteException {
throw new UnsupportedOperationException("Not supported");
}
diff --git a/virtualizationmanager/Android.bp b/virtualizationmanager/Android.bp
index e82797e..c913d02 100644
--- a/virtualizationmanager/Android.bp
+++ b/virtualizationmanager/Android.bp
@@ -60,7 +60,6 @@
"packagemanager_aidl-rust",
],
shared_libs: [
- "libbinder_rpc_unstable",
"libselinux",
],
}
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index aceb319..48e2431 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -19,6 +19,8 @@
write_vm_booted_stats, write_vm_creation_stats};
use crate::composite::make_composite_image;
use crate::crosvm::{CrosvmConfig, DiskFile, PayloadState, VmContext, VmInstance, VmState};
+use crate::debug_config::should_prepare_console_output;
+use crate::debug_config::is_ramdump_needed;
use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images};
use crate::selinux::{getfilecon, SeContext};
use android_os_permissions_aidl::aidl::android::os::IPermissionController;
@@ -317,6 +319,17 @@
check_gdb_allowed(config)?;
}
+ let ramdump = if is_ramdump_needed(config) {
+ Some(prepare_ramdump_file(&temporary_directory)?)
+ } else {
+ None
+ };
+
+ let debug_level = match config {
+ VirtualMachineConfig::AppConfig(app_config) => app_config.debugLevel,
+ _ => DebugLevel::NONE,
+ };
+
let state = &mut *self.state.lock().unwrap();
let console_fd =
clone_or_prepare_logger_fd(config, console_fd, format!("Console({})", cid))?;
@@ -407,19 +420,6 @@
}
};
- // Creating this ramdump file unconditionally is not harmful as ramdump will be created
- // only when the VM is configured as such. `ramdump_write` is sent to crosvm and will
- // be the backing store for the /dev/hvc1 where VM will emit ramdump to. `ramdump_read`
- // will be sent back to the client (i.e. the VM owner) for readout.
- let ramdump_path = temporary_directory.join("ramdump");
- let ramdump = prepare_ramdump_file(&ramdump_path).map_err(|e| {
- error!("Failed to prepare ramdump file: {:?}", e);
- Status::new_service_specific_error_str(
- -1,
- Some(format!("Failed to prepare ramdump file: {:?}", e)),
- )
- })?;
-
// Actually start the VM.
let crosvm_config = CrosvmConfig {
cid,
@@ -430,13 +430,14 @@
disks,
params: config.params.to_owned(),
protected: *is_protected,
+ debug_level,
memory_mib: config.memoryMib.try_into().ok().and_then(NonZeroU32::new),
cpus,
host_cpu_topology,
task_profiles: config.taskProfiles.clone(),
console_fd,
log_fd,
- ramdump: Some(ramdump),
+ ramdump,
indirect_files,
platform_version: parse_platform_version_req(&config.platformVersion)?,
detect_hangup: is_app_config,
@@ -485,10 +486,6 @@
part.flush()
}
-fn prepare_ramdump_file(ramdump_path: &Path) -> Result<File> {
- File::create(ramdump_path).context(format!("Failed to create ramdump file {:?}", &ramdump_path))
-}
-
fn round_up(input: u64, granularity: u64) -> u64 {
if granularity == 0 {
return input;
@@ -978,11 +975,20 @@
})
}
-fn is_debuggable(config: &VirtualMachineConfig) -> bool {
- match config {
- VirtualMachineConfig::AppConfig(config) => config.debugLevel != DebugLevel::NONE,
- _ => false,
- }
+/// Create the empty ramdump file
+fn prepare_ramdump_file(temporary_directory: &Path) -> binder::Result<File> {
+ // `ramdump_write` is sent to crosvm and will be the backing store for the /dev/hvc1 where
+ // VM will emit ramdump to. `ramdump_read` will be sent back to the client (i.e. the VM
+ // owner) for readout.
+ let ramdump_path = temporary_directory.join("ramdump");
+ let ramdump = File::create(ramdump_path).map_err(|e| {
+ error!("Failed to prepare ramdump file: {:?}", e);
+ Status::new_service_specific_error_str(
+ -1,
+ Some(format!("Failed to prepare ramdump file: {:?}", e)),
+ )
+ })?;
+ Ok(ramdump)
}
fn is_protected(config: &VirtualMachineConfig) -> bool {
@@ -1031,9 +1037,12 @@
return Ok(Some(clone_file(fd)?));
}
- if !is_debuggable(config) {
+ let VirtualMachineConfig::AppConfig(app_config) = config else {
return Ok(None);
- }
+ };
+ if !should_prepare_console_output(app_config.debugLevel) {
+ return Ok(None);
+ };
let (raw_read_fd, raw_write_fd) = pipe().map_err(|e| {
Status::new_service_specific_error_str(-1, Some(format!("Failed to create pipe: {:?}", e)))
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 09605a4..9db0971 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -16,6 +16,7 @@
use crate::aidl::{remove_temporary_files, Cid, VirtualMachineCallbacks};
use crate::atom::{get_num_cpus, write_vm_exited_stats};
+use crate::debug_config::should_prepare_console_output;
use anyhow::{anyhow, bail, Context, Error, Result};
use command_fds::CommandFdExt;
use lazy_static::lazy_static;
@@ -41,7 +42,10 @@
use std::time::{Duration, SystemTime};
use std::thread::{self, JoinHandle};
use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::DeathReason::DeathReason;
-use android_system_virtualizationservice::aidl::android::system::virtualizationservice::MemoryTrimLevel::MemoryTrimLevel;
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+ MemoryTrimLevel::MemoryTrimLevel,
+ VirtualMachineAppConfig::DebugLevel::DebugLevel
+};
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;
@@ -68,6 +72,8 @@
const CROSVM_CRASH_STATUS: i32 = 33;
/// The exit status which crosvm returns when vcpu is stalled.
const CROSVM_WATCHDOG_REBOOT_STATUS: i32 = 36;
+/// The size of memory (in MiB) reserved for ramdump
+const RAMDUMP_RESERVED_MIB: u32 = 17;
const MILLIS_PER_SEC: i64 = 1000;
@@ -95,6 +101,7 @@
pub disks: Vec<DiskFile>,
pub params: Option<String>,
pub protected: bool,
+ pub debug_level: DebugLevel,
pub memory_mib: Option<NonZeroU32>,
pub cpus: Option<NonZeroU32>,
pub host_cpu_topology: bool,
@@ -530,6 +537,9 @@
/// Checks if ramdump has been created. If so, send it to tombstoned.
fn handle_ramdump(&self) -> Result<(), Error> {
let ramdump_path = self.temporary_directory.join("ramdump");
+ if !ramdump_path.as_path().try_exists()? {
+ return Ok(());
+ }
if std::fs::metadata(&ramdump_path)?.len() > 0 {
Self::send_ramdump_to_tombstoned(&ramdump_path)?;
}
@@ -665,24 +675,6 @@
}
}
-fn should_configure_ramdump(protected: bool) -> bool {
- if protected {
- // Protected VM needs ramdump configuration here.
- // pvmfw will disable ramdump if unnecessary.
- true
- } else {
- // For unprotected VM, ramdump should be handled here.
- // ramdump wouldn't be enabled if ramdump is explicitly set to <1>.
- if let Ok(mut file) = File::open("/proc/device-tree/avf/guest/common/ramdump") {
- let mut ramdump: [u8; 4] = Default::default();
- file.read_exact(&mut ramdump).map_err(|_| false).unwrap();
- // DT spec uses big endian although Android is always little endian.
- return u32::from_be_bytes(ramdump) == 1;
- }
- false
- }
-}
-
/// Starts an instance of `crosvm` to manage a new VM.
fn run_vm(
config: CrosvmConfig,
@@ -722,8 +714,32 @@
let virtio_pci_device_count = 4 + config.disks.len();
// crosvm virtio queue has 256 entries, so 2 MiB per device (2 pages per entry) should be
// enough.
- let swiotlb_size_mib = 2 * virtio_pci_device_count;
+ let swiotlb_size_mib = 2 * virtio_pci_device_count as u32;
command.arg("--swiotlb").arg(swiotlb_size_mib.to_string());
+
+ // Workaround to keep crash_dump from trying to read protected guest memory.
+ // Context in b/238324526.
+ command.arg("--unmap-guest-memory-on-fork");
+
+ if config.ramdump.is_some() {
+ // Protected VM needs to reserve memory for ramdump here. pvmfw will drop This
+ // if ramdump should be disabled (via debug policy). Note that we reserve more
+ // memory for the restricted dma pool.
+ let ramdump_reserve = RAMDUMP_RESERVED_MIB + swiotlb_size_mib;
+ command.arg("--params").arg(format!("crashkernel={ramdump_reserve}M"));
+ }
+ } else {
+ if config.ramdump.is_some() {
+ command.arg("--params").arg(format!("crashkernel={RAMDUMP_RESERVED_MIB}M"));
+ }
+ if config.debug_level == DebugLevel::NONE
+ && should_prepare_console_output(config.debug_level)
+ {
+ // bootconfig.normal will be used, but we need log.
+ // pvmfw will add following commands by itself, but non-protected VM should do so here.
+ command.arg("--params").arg("printk.devkmsg=on");
+ command.arg("--params").arg("console=hvc0");
+ }
}
if let Some(memory_mib) = config.memory_mib {
@@ -812,10 +828,6 @@
debug!("Preserving FDs {:?}", preserved_fds);
command.preserved_fds(preserved_fds);
- if should_configure_ramdump(config.protected) {
- command.arg("--params").arg("crashkernel=17M");
- }
-
print_crosvm_args(&command);
let result = SharedChild::spawn(&mut command)?;
diff --git a/virtualizationmanager/src/debug_config.rs b/virtualizationmanager/src/debug_config.rs
new file mode 100644
index 0000000..eda854a
--- /dev/null
+++ b/virtualizationmanager/src/debug_config.rs
@@ -0,0 +1,52 @@
+// Copyright 2023, 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.
+
+//! Functions for AVF debug policy and debug level
+
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+ VirtualMachineAppConfig::DebugLevel::DebugLevel, VirtualMachineConfig::VirtualMachineConfig,
+};
+use std::fs::File;
+use std::io::Read;
+
+/// Get debug policy value in bool. It's true iff the value is explicitly set to <1>.
+fn get_debug_policy_bool(path: &'static str) -> Option<bool> {
+ let mut file = File::open(path).ok()?;
+ let mut log: [u8; 4] = Default::default();
+ file.read_exact(&mut log).ok()?;
+ // DT spec uses big endian although Android is always little endian.
+ Some(u32::from_be_bytes(log) == 1)
+}
+
+/// Get whether console output should be configred for VM to leave console and adb log.
+/// Caller should create pipe and prepare for receiving VM log with it.
+pub fn should_prepare_console_output(debug_level: DebugLevel) -> bool {
+ debug_level != DebugLevel::NONE
+ || get_debug_policy_bool("/proc/device-tree/avf/guest/common/log").unwrap_or_default()
+}
+
+/// Decision to support ramdump
+pub fn is_ramdump_needed(config: &VirtualMachineConfig) -> bool {
+ let enabled_in_dp =
+ get_debug_policy_bool("/proc/device-tree/avf/guest/common/ramdump").unwrap_or_default();
+ let debuggable = match config {
+ VirtualMachineConfig::RawConfig(_) => {
+ // custom VMs are considered debuggable for flexibility
+ true
+ }
+ VirtualMachineConfig::AppConfig(config) => config.debugLevel == DebugLevel::FULL,
+ };
+
+ enabled_in_dp || debuggable
+}
diff --git a/virtualizationmanager/src/main.rs b/virtualizationmanager/src/main.rs
index 3f0b64b..bd7f8af 100644
--- a/virtualizationmanager/src/main.rs
+++ b/virtualizationmanager/src/main.rs
@@ -18,6 +18,7 @@
mod atom;
mod composite;
mod crosvm;
+mod debug_config;
mod payload;
mod selinux;
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 5d785de..36edc64 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -152,7 +152,7 @@
}
fn find_empty_payload_apk_path() -> Result<PathBuf, Error> {
- const GLOB_PATTERN: &str = "/apex/com.android.virt/app/**/EmptyPayloadApp.apk";
+ const GLOB_PATTERN: &str = "/apex/com.android.virt/app/**/EmptyPayloadApp*.apk";
let mut entries: Vec<PathBuf> =
glob(GLOB_PATTERN).context("failed to glob")?.filter_map(|e| e.ok()).collect();
if entries.len() > 1 {
diff --git a/vmbase/src/bionic.rs b/vmbase/src/bionic.rs
index 6f88cf6..69da521 100644
--- a/vmbase/src/bionic.rs
+++ b/vmbase/src/bionic.rs
@@ -126,3 +126,145 @@
0
}
}
+
+#[no_mangle]
+extern "C" fn strerror(n: c_int) -> *mut c_char {
+ // Messages taken from errno(1).
+ let s = match n {
+ 0 => "Success",
+ 1 => "Operation not permitted",
+ 2 => "No such file or directory",
+ 3 => "No such process",
+ 4 => "Interrupted system call",
+ 5 => "Input/output error",
+ 6 => "No such device or address",
+ 7 => "Argument list too long",
+ 8 => "Exec format error",
+ 9 => "Bad file descriptor",
+ 10 => "No child processes",
+ 11 => "Resource temporarily unavailable",
+ 12 => "Cannot allocate memory",
+ 13 => "Permission denied",
+ 14 => "Bad address",
+ 15 => "Block device required",
+ 16 => "Device or resource busy",
+ 17 => "File exists",
+ 18 => "Invalid cross-device link",
+ 19 => "No such device",
+ 20 => "Not a directory",
+ 21 => "Is a directory",
+ 22 => "Invalid argument",
+ 23 => "Too many open files in system",
+ 24 => "Too many open files",
+ 25 => "Inappropriate ioctl for device",
+ 26 => "Text file busy",
+ 27 => "File too large",
+ 28 => "No space left on device",
+ 29 => "Illegal seek",
+ 30 => "Read-only file system",
+ 31 => "Too many links",
+ 32 => "Broken pipe",
+ 33 => "Numerical argument out of domain",
+ 34 => "Numerical result out of range",
+ 35 => "Resource deadlock avoided",
+ 36 => "File name too long",
+ 37 => "No locks available",
+ 38 => "Function not implemented",
+ 39 => "Directory not empty",
+ 40 => "Too many levels of symbolic links",
+ 42 => "No message of desired type",
+ 43 => "Identifier removed",
+ 44 => "Channel number out of range",
+ 45 => "Level 2 not synchronized",
+ 46 => "Level 3 halted",
+ 47 => "Level 3 reset",
+ 48 => "Link number out of range",
+ 49 => "Protocol driver not attached",
+ 50 => "No CSI structure available",
+ 51 => "Level 2 halted",
+ 52 => "Invalid exchange",
+ 53 => "Invalid request descriptor",
+ 54 => "Exchange full",
+ 55 => "No anode",
+ 56 => "Invalid request code",
+ 57 => "Invalid slot",
+ 59 => "Bad font file format",
+ 60 => "Device not a stream",
+ 61 => "No data available",
+ 62 => "Timer expired",
+ 63 => "Out of streams resources",
+ 64 => "Machine is not on the network",
+ 65 => "Package not installed",
+ 66 => "Object is remote",
+ 67 => "Link has been severed",
+ 68 => "Advertise error",
+ 69 => "Srmount error",
+ 70 => "Communication error on send",
+ 71 => "Protocol error",
+ 72 => "Multihop attempted",
+ 73 => "RFS specific error",
+ 74 => "Bad message",
+ 75 => "Value too large for defined data type",
+ 76 => "Name not unique on network",
+ 77 => "File descriptor in bad state",
+ 78 => "Remote address changed",
+ 79 => "Can not access a needed shared library",
+ 80 => "Accessing a corrupted shared library",
+ 81 => ".lib section in a.out corrupted",
+ 82 => "Attempting to link in too many shared libraries",
+ 83 => "Cannot exec a shared library directly",
+ 84 => "Invalid or incomplete multibyte or wide character",
+ 85 => "Interrupted system call should be restarted",
+ 86 => "Streams pipe error",
+ 87 => "Too many users",
+ 88 => "Socket operation on non-socket",
+ 89 => "Destination address required",
+ 90 => "Message too long",
+ 91 => "Protocol wrong type for socket",
+ 92 => "Protocol not available",
+ 93 => "Protocol not supported",
+ 94 => "Socket type not supported",
+ 95 => "Operation not supported",
+ 96 => "Protocol family not supported",
+ 97 => "Address family not supported by protocol",
+ 98 => "Address already in use",
+ 99 => "Cannot assign requested address",
+ 100 => "Network is down",
+ 101 => "Network is unreachable",
+ 102 => "Network dropped connection on reset",
+ 103 => "Software caused connection abort",
+ 104 => "Connection reset by peer",
+ 105 => "No buffer space available",
+ 106 => "Transport endpoint is already connected",
+ 107 => "Transport endpoint is not connected",
+ 108 => "Cannot send after transport endpoint shutdown",
+ 109 => "Too many references: cannot splice",
+ 110 => "Connection timed out",
+ 111 => "Connection refused",
+ 112 => "Host is down",
+ 113 => "No route to host",
+ 114 => "Operation already in progress",
+ 115 => "Operation now in progress",
+ 116 => "Stale file handle",
+ 117 => "Structure needs cleaning",
+ 118 => "Not a XENIX named type file",
+ 119 => "No XENIX semaphores available",
+ 120 => "Is a named type file",
+ 121 => "Remote I/O error",
+ 122 => "Disk quota exceeded",
+ 123 => "No medium found",
+ 124 => "Wrong medium type",
+ 125 => "Operation canceled",
+ 126 => "Required key not available",
+ 127 => "Key has expired",
+ 128 => "Key has been revoked",
+ 129 => "Key was rejected by service",
+ 130 => "Owner died",
+ 131 => "State not recoverable",
+ 132 => "Operation not possible due to RF-kill",
+ 133 => "Memory page has hardware error",
+ _ => "Unknown errno value",
+ };
+
+ s.as_ptr().cast_mut().cast()
+}
diff --git a/vmclient/Android.bp b/vmclient/Android.bp
index 0a2e692..8517c88 100644
--- a/vmclient/Android.bp
+++ b/vmclient/Android.bp
@@ -18,9 +18,6 @@
"libshared_child",
"libthiserror",
],
- shared_libs: [
- "libbinder_rpc_unstable",
- ],
apex_available: [
"com.android.compos",
"com.android.virt",
diff --git a/vmclient/src/lib.rs b/vmclient/src/lib.rs
index 0e3d140..77ddf05 100644
--- a/vmclient/src/lib.rs
+++ b/vmclient/src/lib.rs
@@ -122,7 +122,7 @@
let session = RpcSession::new();
session.set_file_descriptor_transport_mode(FileDescriptorTransportMode::Unix);
session.set_max_incoming_threads(VIRTMGR_THREADS);
- session.set_max_outgoing_threads(VIRTMGR_THREADS);
+ session.set_max_outgoing_connections(VIRTMGR_THREADS);
session
.setup_unix_domain_bootstrap_client(self.client_fd.as_fd())
.map_err(|_| io::Error::from(io::ErrorKind::ConnectionRefused))
diff --git a/zipfuse/src/inode.rs b/zipfuse/src/inode.rs
index 3edbc49..ea63422 100644
--- a/zipfuse/src/inode.rs
+++ b/zipfuse/src/inode.rs
@@ -99,12 +99,8 @@
InodeData { mode, size: 0, data: InodeDataData::Directory(HashMap::new()) }
}
- fn new_file(zip_index: ZipIndex, zip_file: &zip::read::ZipFile) -> InodeData {
- InodeData {
- mode: zip_file.unix_mode().unwrap_or(DEFAULT_FILE_MODE),
- size: zip_file.size(),
- data: InodeDataData::File(zip_index),
- }
+ fn new_file(zip_index: ZipIndex, mode: u32, zip_file: &zip::read::ZipFile) -> InodeData {
+ InodeData { mode, size: zip_file.size(), data: InodeDataData::File(zip_index) }
}
fn add_to_directory(&mut self, name: CString, entry: DirectoryEntry) {
@@ -188,6 +184,16 @@
let mut parent = ROOT;
let mut iter = path.iter().peekable();
+
+ let mut file_mode = DEFAULT_FILE_MODE;
+ if path.starts_with("bin/") {
+ // Allow files under bin to have execute permission, this enables payloads to bundle
+ // additional binaries that they might want to execute.
+ // An example of such binary is measure_io one used in the authfs performance tests.
+ // More context available at b/265261525 and b/270955654.
+ file_mode |= libc::S_IXUSR;
+ }
+
while let Some(name) = iter.next() {
// TODO(jiyong): remove this check by canonicalizing `path`
if name == ".." {
@@ -211,8 +217,11 @@
}
// No inode found. Create a new inode and add it to the inode table.
+ // At the moment of writing this comment the apk file doesn't specify any
+ // permissions (apart from the ones on lib/), but it might change in the future.
+ // TODO(b/270955654): should we control the file permissions ourselves?
let inode = if is_file {
- InodeData::new_file(i, &file)
+ InodeData::new_file(i, file.unix_mode().unwrap_or(file_mode), &file)
} else if is_leaf {
InodeData::new_dir(file.unix_mode().unwrap_or(DEFAULT_DIR_MODE))
} else {