Merge "MicrodroidHostTest: mkdir test dir @Before test run"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 14452a3..3217ee1 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -4,9 +4,6 @@
"name": "MicrodroidHostTestCases"
},
{
- "name": "ComposHostTestCases"
- },
- {
"name": "MicrodroidTestApp"
},
{
@@ -36,6 +33,9 @@
"name": "ComposBenchmarkApp"
},
{
+ "name": "ComposHostTestCases"
+ },
+ {
"name": "AVFHostTestCases"
}
],
@@ -49,9 +49,6 @@
"path": "packages/modules/Virtualization/apkdmverity"
},
{
- "path": "packages/modules/Virtualization/avmd"
- },
- {
"path": "packages/modules/Virtualization/encryptedstore"
},
{
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..db1fd44 100644
--- a/authfs/fd_server/Android.bp
+++ b/authfs/fd_server/Android.bp
@@ -12,15 +12,13 @@
"libauthfs_fsverity_metadata",
"libbinder_rs",
"libclap",
+ "libfsverity_rs",
"liblibc",
"liblog_rust",
"libnix",
"librpcbinder_rs",
],
prefer_rlib: true,
- shared_libs: [
- "libbinder_rpc_unstable",
- ],
apex_available: ["com.android.virt"],
}
@@ -34,14 +32,12 @@
"libauthfs_fsverity_metadata",
"libbinder_rs",
"libclap",
+ "libfsverity_rs",
"liblibc",
"liblog_rust",
"libnix",
"librpcbinder_rs",
],
prefer_rlib: true,
- shared_libs: [
- "libbinder_rpc_unstable",
- ],
test_suites: ["general-tests"],
}
diff --git a/authfs/fd_server/src/aidl.rs b/authfs/fd_server/src/aidl.rs
index 01b8209..ada3ffb 100644
--- a/authfs/fd_server/src/aidl.rs
+++ b/authfs/fd_server/src/aidl.rs
@@ -31,7 +31,6 @@
use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR};
use std::sync::{Arc, RwLock};
-use crate::fsverity;
use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::{
BnVirtFdService, FsStat::FsStat, IVirtFdService, MAX_REQUESTING_DATA,
};
diff --git a/authfs/fd_server/src/fsverity.rs b/authfs/fd_server/src/fsverity.rs
deleted file mode 100644
index 576f9dd..0000000
--- a/authfs/fd_server/src/fsverity.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-use nix::ioctl_readwrite;
-use std::io;
-
-// Constants/values from uapi/linux/fsverity.h
-const FS_VERITY_METADATA_TYPE_MERKLE_TREE: u64 = 1;
-const FS_VERITY_METADATA_TYPE_SIGNATURE: u64 = 3;
-const FS_IOCTL_MAGIC: u8 = b'f';
-const FS_IOCTL_READ_VERITY_METADATA: u8 = 135;
-
-#[repr(C)]
-pub struct fsverity_read_metadata_arg {
- metadata_type: u64,
- offset: u64,
- length: u64,
- buf_ptr: u64,
- __reserved: u64,
-}
-
-ioctl_readwrite!(
- read_verity_metadata,
- FS_IOCTL_MAGIC,
- FS_IOCTL_READ_VERITY_METADATA,
- fsverity_read_metadata_arg
-);
-
-fn read_metadata(fd: i32, metadata_type: u64, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
- let mut arg = fsverity_read_metadata_arg {
- metadata_type,
- offset,
- length: buf.len() as u64,
- buf_ptr: buf.as_mut_ptr() as u64,
- __reserved: 0,
- };
- Ok(unsafe { read_verity_metadata(fd, &mut arg) }? as usize)
-}
-
-/// Read the raw Merkle tree from the fd, if it exists. The API semantics is similar to a regular
-/// pread(2), and may not return full requested buffer.
-pub fn read_merkle_tree(fd: i32, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
- read_metadata(fd, FS_VERITY_METADATA_TYPE_MERKLE_TREE, offset, buf)
-}
-
-/// Read the fs-verity signature from the fd (if exists). The returned signature should be complete.
-pub fn read_signature(fd: i32, buf: &mut [u8]) -> io::Result<usize> {
- read_metadata(fd, FS_VERITY_METADATA_TYPE_SIGNATURE, 0 /* offset */, buf)
-}
diff --git a/authfs/fd_server/src/main.rs b/authfs/fd_server/src/main.rs
index f91ebec..47983cb 100644
--- a/authfs/fd_server/src/main.rs
+++ b/authfs/fd_server/src/main.rs
@@ -23,7 +23,6 @@
//! client can then request the content of file 9 by offset and size.
mod aidl;
-mod fsverity;
use anyhow::{bail, Result};
use clap::Parser;
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/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java b/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java
index 357edea..7c85797 100644
--- a/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java
+++ b/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java
@@ -88,7 +88,7 @@
private static CommandRunner sAndroid;
private static CommandRunner sMicrodroid;
- private final ExecutorService mThreadPool = Executors.newCachedThreadPool();
+ private ExecutorService mThreadPool;
public static void setUpAndroid(TestInformation testInfo) throws Exception {
assertNotNull(testInfo.getDevice());
@@ -242,6 +242,7 @@
}
public void setUpTest() throws Exception {
+ mThreadPool = Executors.newCachedThreadPool();
if (sAndroid != null) {
sAndroid.run("mkdir -p " + TEST_OUTPUT_DIR);
}
@@ -264,5 +265,10 @@
archiveLogThenDelete(this, getDevice(), vmRecentLog, "vm_recent.log-" + testName);
sAndroid.run("rm -rf " + TEST_OUTPUT_DIR);
+
+ if (mThreadPool != null) {
+ mThreadPool.shutdownNow();
+ mThreadPool = null;
+ }
}
}
diff --git a/avmd/Android.bp b/avmd/Android.bp
deleted file mode 100644
index e5e0553..0000000
--- a/avmd/Android.bp
+++ /dev/null
@@ -1,61 +0,0 @@
-package {
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-rust_defaults {
- name: "libavmd_defaults",
- crate_name: "avmd",
- host_supported: true,
- srcs: ["src/lib.rs"],
- prefer_rlib: true,
- rustlibs: [
- "libhex",
- "libserde",
- "libapkverify",
- ],
-}
-
-rust_library {
- name: "libavmd",
- defaults: ["libavmd_defaults"],
-}
-
-rust_defaults {
- name: "avmdtool.defaults",
- srcs: ["src/main.rs"],
- host_supported: true,
- prefer_rlib: true,
- rustlibs: [
- "libanyhow",
- "libapexutil_rust",
- "libapkverify",
- "libavmd",
- "libclap",
- "libserde",
- "libserde_cbor",
- "libvbmeta_rust",
- ],
-}
-
-rust_binary {
- name: "avmdtool",
- defaults: ["avmdtool.defaults"],
-}
-
-rust_test {
- name: "avmdtool.test",
- defaults: ["avmdtool.defaults"],
- test_suites: ["general-tests"],
-}
-
-rust_test {
- name: "avmdtool_tests",
- srcs: ["tests/*_test.rs"],
- test_suites: ["general-tests"],
- rustlibs: [
- "libtempfile",
- ],
- compile_multilib: "first",
- data_bins: ["avmdtool"],
- data: ["tests/data/*"],
-}
diff --git a/avmd/README.md b/avmd/README.md
deleted file mode 100644
index ae813a0..0000000
--- a/avmd/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# The AVMD image format
----
-
-The AVMD image format is used to descibe the verified code that a VM will
-load. This repository contains tools and libraries for working with the AVMD
-image format.
-
-# What is it?
-
-When a VM boots, it loads and verifies a set of images that control execution
-within the VM. Therefore, describing what executes in a VM means describing
-what is loaded. The AVMD image format is designed, for this purpose, to
-describe the closure of images that can be loaded and how they should be
-verified.
-
-# Caveats
-
-The AVMD image format will only allow Android supported signing formats. The
-supported formats are currently limited to [AVB][] and [APK][].
-
-[AVB]: https://android.googlesource.com/platform/external/avb/+/master/README.md
-[APK]: https://source.android.com/security/apksigning#schemes
-
-Verification of the images as they are loaded is the responsibility of the VM.
-The VM is required to only load the images described and to verify them against
-the included parameters. If the VM does not follow this requirement, the
-description of the VM may not be accurate and must not be trusted. Validating
-that the VM behaves as expected requires audit of all boot stages of the VM.
-
-# Using avmdtool
-
-The `.avmd` file can be created as follows
-
-```bash
-avmdtool create /tmp/out.avmd \
- --vbmeta pvmfw preload u-boot.bin \
- --vbmeta uboot env_vbmeta disk1/vbmeta.imb \
- --vbmeta uboot vbmeta micordoid/vbmeta.img \
- --apk microdroid payload compos.apk \
- --apk microdroid extra_apk extra_apk.apk \
- --apex-payload microdroid art_apex art.apex
-```
-
-You can read the `.avmd` file with
-
-```bash
-avmdtool dump /tmp/out.avmd
-```
diff --git a/avmd/TEST_MAPPING b/avmd/TEST_MAPPING
deleted file mode 100644
index 892eb2c..0000000
--- a/avmd/TEST_MAPPING
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "avf-presubmit": [
- {
- "name": "avmdtool.test"
- },
- {
- "name": "avmdtool_tests"
- }
- ]
-}
diff --git a/avmd/src/avmd.rs b/avmd/src/avmd.rs
deleted file mode 100644
index cb02f39..0000000
--- a/avmd/src/avmd.rs
+++ /dev/null
@@ -1,154 +0,0 @@
-// Copyright 2022, The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-extern crate alloc;
-
-use alloc::{
- string::{String, ToString},
- vec::Vec,
-};
-use apkverify::SignatureAlgorithmID;
-use core::fmt;
-use serde::{Deserialize, Serialize};
-
-/// An Avmd struct contains
-/// - A header with version information that allows rollback when needed.
-/// - A list of descriptors that describe different images.
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct Avmd {
- header: Header,
- descriptors: Vec<Descriptor>,
-}
-
-impl fmt::Display for Avmd {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- writeln!(f, "Descriptors:")?;
- for descriptor in &self.descriptors {
- write!(f, "{}", descriptor)?;
- }
- Ok(())
- }
-}
-
-impl Avmd {
- /// Creates an instance of Avmd with a given list of descriptors.
- pub fn new(descriptors: Vec<Descriptor>) -> Avmd {
- Avmd { header: Header::default(), descriptors }
- }
-}
-
-static AVMD_MAGIC: u32 = 0x444d5641;
-static AVMD_VERSION_MAJOR: u16 = 1;
-static AVMD_VERSION_MINOR: u16 = 0;
-
-/// Header information for AVMD.
-#[derive(Serialize, Deserialize, Debug, Clone)]
-struct Header {
- magic: u32,
- version_major: u16,
- version_minor: u16,
-}
-
-impl Default for Header {
- fn default() -> Self {
- Header {
- magic: AVMD_MAGIC,
- version_major: AVMD_VERSION_MAJOR,
- version_minor: AVMD_VERSION_MINOR,
- }
- }
-}
-
-/// AVMD descriptor.
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub enum Descriptor {
- /// Descriptor type for the VBMeta images.
- VbMeta(VbMetaDescriptor),
- /// Descriptor type for APK.
- Apk(ApkDescriptor),
-}
-
-impl fmt::Display for Descriptor {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- Descriptor::VbMeta(descriptor) => write!(f, "{}", descriptor),
- Descriptor::Apk(descriptor) => write!(f, "{}", descriptor),
- }
- }
-}
-
-/// VbMeta descriptor.
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct VbMetaDescriptor {
- /// The identifier of this resource.
- #[serde(flatten)]
- pub resource: ResourceIdentifier,
- /// The SHA-512 [VBMeta digest][] calculated from the top-level VBMeta image.
- ///
- /// [VBMeta digest]: https://android.googlesource.com/platform/external/avb/+/master/README.md#the-vbmeta-digest
- pub vbmeta_digest: Vec<u8>,
-}
-
-impl fmt::Display for VbMetaDescriptor {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- writeln!(f, " VBMeta descriptor:")?;
- writeln!(f, " namespace: {}", self.resource.namespace)?;
- writeln!(f, " name: {}", self.resource.name)?;
- writeln!(f, " vbmeta digest: {}", hex::encode(&self.vbmeta_digest))?;
- Ok(())
- }
-}
-
-/// APK descriptor.
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct ApkDescriptor {
- /// The identifier of this resource.
- #[serde(flatten)]
- pub resource: ResourceIdentifier,
- /// The ID of the algoithm used to sign the APK.
- /// It should be one of the algorithms in the [list][].
- ///
- /// [list]: https://source.android.com/security/apksigning/v2#signature-algorithm-ids
- pub signature_algorithm_id: SignatureAlgorithmID,
- /// Digest of the APK's v3 signing block. TODO: fix
- pub apk_digest: Vec<u8>,
-}
-
-impl fmt::Display for ApkDescriptor {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- writeln!(f, " APK descriptor:")?;
- writeln!(f, " namespace: {}", self.resource.namespace)?;
- writeln!(f, " name: {}", self.resource.name)?;
- writeln!(f, " Signing algorithm ID: {:#04x}", self.signature_algorithm_id.to_u32())?;
- writeln!(f, " APK digest: {}", hex::encode(&self.apk_digest))?;
- Ok(())
- }
-}
-
-/// Resource identifier regroups information to identify resources.
-#[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct ResourceIdentifier {
- /// Namespace of the resource.
- namespace: String,
- /// Name of the resource.
- name: String,
-}
-
-impl ResourceIdentifier {
- /// Creates an instance of ResourceIdentifier with the given
- /// namespace and name.
- pub fn new(namespace: &str, name: &str) -> ResourceIdentifier {
- ResourceIdentifier { namespace: namespace.to_string(), name: name.to_string() }
- }
-}
diff --git a/avmd/src/lib.rs b/avmd/src/lib.rs
deleted file mode 100644
index 7a06e6a..0000000
--- a/avmd/src/lib.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright 2022, The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//! Library for handling AVMD blobs.
-
-#![no_std]
-
-mod avmd;
-
-pub use avmd::{ApkDescriptor, Avmd, Descriptor, ResourceIdentifier, VbMetaDescriptor};
diff --git a/avmd/src/main.rs b/avmd/src/main.rs
deleted file mode 100644
index 8d7cb57..0000000
--- a/avmd/src/main.rs
+++ /dev/null
@@ -1,176 +0,0 @@
-// Copyright 2022, The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//! Tool for handling AVMD blobs.
-
-use anyhow::{anyhow, bail, Result};
-use apexutil::get_payload_vbmeta_image_hash;
-use apkverify::get_apk_digest;
-use avmd::{ApkDescriptor, Avmd, Descriptor, ResourceIdentifier, VbMetaDescriptor};
-use clap::{
- builder::ValueParser,
- parser::{Indices, ValuesRef},
- Arg, ArgAction, ArgMatches, Command,
-};
-use serde::ser::Serialize;
-use std::{fs::File, path::PathBuf};
-use vbmeta::VbMetaImage;
-
-fn get_vbmeta_image_hash(file: &str) -> Result<Vec<u8>> {
- let img = VbMetaImage::verify_path(file)?;
- Ok(img.hash().ok_or_else(|| anyhow!("No hash as VBMeta image isn't signed"))?.to_vec())
-}
-
-/// Iterate over a set of argument values, that could be empty or come in
-/// (<index>, <namespace>, <name>, <file>) tuple.
-struct NamespaceNameFileIterator<'a> {
- indices: Option<Indices<'a>>,
- values: Option<ValuesRef<'a, String>>,
-}
-
-impl<'a> NamespaceNameFileIterator<'a> {
- fn new(args: &'a ArgMatches, name: &'a str) -> Self {
- NamespaceNameFileIterator { indices: args.indices_of(name), values: args.get_many(name) }
- }
-}
-
-impl<'a> Iterator for NamespaceNameFileIterator<'a> {
- type Item = (usize, &'a str, &'a str, &'a str);
-
- fn next(&mut self) -> Option<Self::Item> {
- match (self.indices.as_mut(), self.values.as_mut()) {
- (Some(indices), Some(values)) => {
- match (indices.nth(2), values.next(), values.next(), values.next()) {
- (Some(index), Some(namespace), Some(name), Some(file)) => {
- Some((index, namespace, name, file))
- }
- _ => None,
- }
- }
- _ => None,
- }
- }
-}
-
-fn create(args: &ArgMatches) -> Result<()> {
- // Store descriptors in the order they were given in the arguments
- // TODO: instead, group them by namespace?
- let mut descriptors = std::collections::BTreeMap::new();
- for (i, namespace, name, file) in NamespaceNameFileIterator::new(args, "vbmeta") {
- descriptors.insert(
- i,
- Descriptor::VbMeta(VbMetaDescriptor {
- resource: ResourceIdentifier::new(namespace, name),
- vbmeta_digest: get_vbmeta_image_hash(file)?,
- }),
- );
- }
- for (i, namespace, name, file) in NamespaceNameFileIterator::new(args, "apk") {
- let file = File::open(file)?;
- let (signature_algorithm_id, apk_digest) = get_apk_digest(file, /*verify=*/ true)?;
- descriptors.insert(
- i,
- Descriptor::Apk(ApkDescriptor {
- resource: ResourceIdentifier::new(namespace, name),
- signature_algorithm_id,
- apk_digest: apk_digest.to_vec(),
- }),
- );
- }
- for (i, namespace, name, file) in NamespaceNameFileIterator::new(args, "apex-payload") {
- descriptors.insert(
- i,
- Descriptor::VbMeta(VbMetaDescriptor {
- resource: ResourceIdentifier::new(namespace, name),
- vbmeta_digest: get_payload_vbmeta_image_hash(file)?,
- }),
- );
- }
- let avmd = Avmd::new(descriptors.into_values().collect());
- let mut bytes = Vec::new();
- avmd.serialize(
- &mut serde_cbor::Serializer::new(&mut serde_cbor::ser::IoWrite::new(&mut bytes))
- .packed_format()
- .legacy_enums(),
- )?;
- std::fs::write(args.get_one::<PathBuf>("file").unwrap(), &bytes)?;
- Ok(())
-}
-
-fn dump(args: &ArgMatches) -> Result<()> {
- let file = std::fs::read(args.get_one::<PathBuf>("file").unwrap())?;
- let avmd: Avmd = serde_cbor::from_slice(&file)?;
- println!("{}", avmd);
- Ok(())
-}
-
-fn clap_command() -> Command {
- let namespace_name_file = ["namespace", "name", "file"];
-
- Command::new("avmdtool")
- .subcommand_required(true)
- .arg_required_else_help(true)
- .subcommand(
- Command::new("create")
- .arg_required_else_help(true)
- .arg(Arg::new("file").value_parser(ValueParser::path_buf()).required(true))
- .arg(
- Arg::new("vbmeta")
- .long("vbmeta")
- .value_names(namespace_name_file)
- .num_args(3)
- .action(ArgAction::Append),
- )
- .arg(
- Arg::new("apk")
- .long("apk")
- .value_names(namespace_name_file)
- .num_args(3)
- .action(ArgAction::Append),
- )
- .arg(
- Arg::new("apex-payload")
- .long("apex-payload")
- .value_names(namespace_name_file)
- .num_args(3)
- .action(ArgAction::Append),
- ),
- )
- .subcommand(
- Command::new("dump")
- .arg_required_else_help(true)
- .arg(Arg::new("file").value_parser(ValueParser::path_buf()).required(true)),
- )
-}
-
-fn main() -> Result<()> {
- let args = clap_command().get_matches();
- match args.subcommand() {
- Some(("create", sub_args)) => create(sub_args)?,
- Some(("dump", sub_args)) => dump(sub_args)?,
- _ => bail!("Invalid arguments"),
- }
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn verify_command() {
- // Check that the command parsing has been configured in a valid way.
- clap_command().debug_assert();
- }
-}
diff --git a/avmd/tests/avmdtool_test.rs b/avmd/tests/avmdtool_test.rs
deleted file mode 100644
index 4647f06..0000000
--- a/avmd/tests/avmdtool_test.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright 2022, The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//! Tests for avmdtool.
-
-use std::fs;
-use std::process::Command;
-use tempfile::TempDir;
-
-#[test]
-fn test_dump() {
- let filename = "tests/data/test.avmd";
- assert!(
- fs::metadata(filename).is_ok(),
- "File '{}' does not exist. You can re-create it with:
- avmdtool create {} \\
- --apex-payload microdroid vbmeta tests/data/test.apex \\
- --apk microdroid_manager apk \\
- tests/data/v3-only-with-rsa-pkcs1-sha256-4096.apk \\
- --apk microdroid_manager extra-apk tests/data/v3-only-with-stamp.apk",
- filename,
- filename
- );
- let output = Command::new("./avmdtool").args(["dump", filename]).output().unwrap();
- assert!(output.status.success());
- assert_eq!(output.stdout, fs::read("tests/data/test.avmd.dump").unwrap());
-}
-
-#[test]
-fn test_create() {
- let test_dir = TempDir::new().unwrap();
- let test_file_path = test_dir.path().join("tmp_test.amvd");
- let output = Command::new("./avmdtool")
- .args([
- "create",
- test_file_path.to_str().unwrap(),
- "--apex-payload",
- "microdroid",
- "vbmeta",
- "tests/data/test.apex",
- "--apk",
- "microdroid_manager",
- "apk",
- "tests/data/v3-only-with-rsa-pkcs1-sha256-4096.apk",
- "--apk",
- "microdroid_manager",
- "extra-apk",
- "tests/data/v3-only-with-stamp.apk",
- ])
- .output()
- .unwrap();
- assert!(output.status.success());
- assert_eq!(fs::read(test_file_path).unwrap(), fs::read("tests/data/test.avmd").unwrap());
-}
diff --git a/avmd/tests/data/test.apex b/avmd/tests/data/test.apex
deleted file mode 100644
index fd79365..0000000
--- a/avmd/tests/data/test.apex
+++ /dev/null
Binary files differ
diff --git a/avmd/tests/data/test.avmd b/avmd/tests/data/test.avmd
deleted file mode 100644
index e567125..0000000
--- a/avmd/tests/data/test.avmd
+++ /dev/null
Binary files differ
diff --git a/avmd/tests/data/test.avmd.dump b/avmd/tests/data/test.avmd.dump
deleted file mode 100644
index a63a151..0000000
--- a/avmd/tests/data/test.avmd.dump
+++ /dev/null
@@ -1,16 +0,0 @@
-Descriptors:
- VBMeta descriptor:
- namespace: microdroid
- name: vbmeta
- vbmeta digest: 296e32a76544de9da01713e471403ab4667705ad527bb4f1fac0cf61e7ce122d
- APK descriptor:
- namespace: microdroid_manager
- name: apk
- Signing algorithm ID: 0x103
- APK digest: 0df2426ea33aedaf495d88e5be0c6a1663ff0a81c5ed12d5b2929ae4b4300f2f
- APK descriptor:
- namespace: microdroid_manager
- name: extra-apk
- Signing algorithm ID: 0x201
- APK digest: 626bb647c0089717a7ffa52fd8e845f9403d5e27f7a5a8752e47b3345fb82f5c
-
diff --git a/avmd/tests/data/v3-only-with-rsa-pkcs1-sha256-4096.apk b/avmd/tests/data/v3-only-with-rsa-pkcs1-sha256-4096.apk
deleted file mode 100644
index 0c9391c..0000000
--- a/avmd/tests/data/v3-only-with-rsa-pkcs1-sha256-4096.apk
+++ /dev/null
Binary files differ
diff --git a/avmd/tests/data/v3-only-with-stamp.apk b/avmd/tests/data/v3-only-with-stamp.apk
deleted file mode 100644
index 5f65214..0000000
--- a/avmd/tests/data/v3-only-with-stamp.apk
+++ /dev/null
Binary files differ
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 f6811cb..96c8147 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -22,30 +22,41 @@
COMPOS_APEX_ROOT, COMPOS_DATA_ROOT, COMPOS_VSOCK_PORT,
};
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+ CpuTopology::CpuTopology,
IVirtualizationService::IVirtualizationService,
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::num::NonZeroU32;
+use std::fs::File;
use std::path::{Path, PathBuf};
use vmclient::{DeathReason, ErrorCode, VmInstance, VmWaitError};
/// This owns an instance of the CompOS VM.
pub struct ComposClient(VmInstance);
+/// CPU topology configuration for a virtual machine.
+#[derive(Default, Debug, Clone)]
+pub enum VmCpuTopology {
+ /// Run VM with 1 vCPU only.
+ #[default]
+ OneCpu,
+ /// Run VM vCPU topology matching that of the host.
+ MatchHost,
+}
+
/// Parameters to be used when creating a virtual machine instance.
#[derive(Default, Debug, Clone)]
pub struct VmParameters {
/// Whether the VM should be debuggable.
pub debug_mode: bool,
- /// Number of vCPUs to have in the VM. If None, defaults to 1.
- pub cpus: Option<NonZeroU32>,
+ /// CPU topology of the VM. Defaults to 1 vCPU.
+ pub cpu_topology: VmCpuTopology,
/// List of task profiles to apply to the VM
pub task_profiles: Vec<String>,
/// If present, overrides the amount of RAM to give the VM
@@ -111,6 +122,11 @@
(Some(console_fd), Some(log_fd))
};
+ let cpu_topology = match parameters.cpu_topology {
+ VmCpuTopology::OneCpu => CpuTopology::ONE_CPU,
+ VmCpuTopology::MatchHost => CpuTopology::MATCH_HOST,
+ };
+
let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
name: String::from("Compos"),
apk: Some(apk_fd),
@@ -122,8 +138,9 @@
extraIdsigs: extra_idsigs,
protectedVm: protected_vm,
memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default
- numCpus: parameters.cpus.map_or(1, NonZeroU32::get) as i32,
+ cpuTopology: cpu_topology,
taskProfiles: parameters.task_profiles.clone(),
+ gdbPort: 0, // Don't start gdb-server
});
let callback = Box::new(Callback {});
@@ -178,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/compos/composd/Android.bp b/compos/composd/Android.bp
index 07a9be3..b0294dd 100644
--- a/compos/composd/Android.bp
+++ b/compos/composd/Android.bp
@@ -16,11 +16,13 @@
"libbinder_rs",
"libcompos_common",
"libcomposd_native_rust",
+ "libfsverity_rs",
"libminijail_rust",
- "libnum_cpus",
"libnix",
"liblibc",
"liblog_rust",
+ "libodsign_proto_rust",
+ "libprotobuf",
"librustutils",
"libshared_child",
"libvmclient",
diff --git a/compos/composd/aidl/android/system/composd/ICompilationTaskCallback.aidl b/compos/composd/aidl/android/system/composd/ICompilationTaskCallback.aidl
index 569bba5..a3ce553 100644
--- a/compos/composd/aidl/android/system/composd/ICompilationTaskCallback.aidl
+++ b/compos/composd/aidl/android/system/composd/ICompilationTaskCallback.aidl
@@ -25,6 +25,8 @@
CompilationFailed,
/** We ran compilation in the VM, but it reported a problem. */
UnexpectedCompilationResult,
+ /** We failed to enable fs-verity completely to the output artifacts. */
+ FailedToEnableFsverity,
}
/**
diff --git a/compos/composd/src/instance_manager.rs b/compos/composd/src/instance_manager.rs
index 0a6c3d6..98d4a1b 100644
--- a/compos/composd/src/instance_manager.rs
+++ b/compos/composd/src/instance_manager.rs
@@ -21,14 +21,13 @@
use android_system_virtualizationservice::aidl::android::system::virtualizationservice;
use anyhow::{bail, Result};
use binder::Strong;
-use compos_common::compos_client::VmParameters;
+use compos_common::compos_client::{VmCpuTopology, VmParameters};
use compos_common::{CURRENT_INSTANCE_DIR, TEST_INSTANCE_DIR};
-use std::num::NonZeroU32;
use std::sync::{Arc, Mutex, Weak};
use virtualizationservice::IVirtualizationService::IVirtualizationService;
// Enough memory to complete odrefresh in the VM.
-const VM_MEMORY_MIB: i32 = 1280;
+const VM_MEMORY_MIB: i32 = 1024;
pub struct InstanceManager {
service: Strong<dyn IVirtualizationService>,
@@ -80,9 +79,14 @@
// By default, dex2oat starts as many threads as there are CPUs. This can be overridden with
// a system property. Start the VM with all CPUs and assume the guest will start a suitable
// number of dex2oat threads.
- let cpus = NonZeroU32::new(num_cpus::get() as u32);
+ let cpu_topology = VmCpuTopology::MatchHost;
let task_profiles = vec!["SCHED_SP_COMPUTE".to_string()];
- Ok(VmParameters { cpus, task_profiles, memory_mib: Some(VM_MEMORY_MIB), ..Default::default() })
+ Ok(VmParameters {
+ cpu_topology,
+ task_profiles,
+ memory_mib: Some(VM_MEMORY_MIB),
+ ..Default::default()
+ })
}
// Ensures we only run one instance at a time.
diff --git a/compos/composd/src/odrefresh_task.rs b/compos/composd/src/odrefresh_task.rs
index 3a699ab..a98f50d 100644
--- a/compos/composd/src/odrefresh_task.rs
+++ b/compos/composd/src/odrefresh_task.rs
@@ -28,11 +28,16 @@
CompilationMode::CompilationMode, ICompOsService, OdrefreshArgs::OdrefreshArgs,
};
use compos_common::odrefresh::{
- is_system_property_interesting, ExitCode, ODREFRESH_OUTPUT_ROOT_DIR,
+ is_system_property_interesting, ExitCode, CURRENT_ARTIFACTS_SUBDIR, ODREFRESH_OUTPUT_ROOT_DIR,
+ PENDING_ARTIFACTS_SUBDIR,
};
+use compos_common::BUILD_MANIFEST_SYSTEM_EXT_APK_PATH;
use log::{error, info, warn};
+use odsign_proto::odsign_info::OdsignInfo;
+use protobuf::Message;
use rustutils::system_properties;
-use std::fs::{remove_dir_all, OpenOptions};
+use std::fs::{remove_dir_all, File, OpenOptions};
+use std::os::fd::AsFd;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::{AsRawFd, OwnedFd};
use std::path::Path;
@@ -103,8 +108,21 @@
let result = match exit_code {
Ok(ExitCode::CompilationSuccess) => {
- info!("CompilationSuccess");
- callback.onSuccess()
+ if compilation_mode == CompilationMode::TEST_COMPILE {
+ info!("Compilation success");
+ callback.onSuccess()
+ } else {
+ // compos.info is generated only during NORMAL_COMPILE
+ if let Err(e) = enable_fsverity_to_all() {
+ let message =
+ format!("Unexpected failure when enabling fs-verity: {:?}", e);
+ error!("{}", message);
+ callback.onFailure(FailureReason::FailedToEnableFsverity, &message)
+ } else {
+ info!("Compilation success, fs-verity enabled");
+ callback.onSuccess()
+ }
+ }
}
Ok(exit_code) => {
let message = format!("Unexpected odrefresh result: {:?}", exit_code);
@@ -161,13 +179,20 @@
let output_dir_raw_fd = output_dir_fd.as_raw_fd();
let staging_dir_raw_fd = staging_dir_fd.as_raw_fd();
- // Get the /system_ext FD differently because it may not exist.
- let (system_ext_dir_raw_fd, ro_dir_fds) =
- if let Ok(system_ext_dir_fd) = open_dir(Path::new("/system_ext")) {
- (system_ext_dir_fd.as_raw_fd(), vec![system_dir_fd, system_ext_dir_fd])
- } else {
- (-1, vec![system_dir_fd])
- };
+ // When the VM starts, it starts with or without mouting the extra build manifest APK from
+ // /system_ext. Later on request (here), we need to pass the directory FD of /system_ext, but
+ // only if the VM is configured to need it.
+ //
+ // It is possible to plumb the information from ComposClient to here, but it's extra complexity
+ // and feel slightly weird to encode the VM's state to the task itself, as it is a request to
+ // the VM.
+ let need_system_ext = Path::new(BUILD_MANIFEST_SYSTEM_EXT_APK_PATH).exists();
+ let (system_ext_dir_raw_fd, ro_dir_fds) = if need_system_ext {
+ let system_ext_dir_fd = open_dir(Path::new("/system_ext"))?;
+ (system_ext_dir_fd.as_raw_fd(), vec![system_dir_fd, system_ext_dir_fd])
+ } else {
+ (-1, vec![system_dir_fd])
+ };
// Spawn a fd_server to serve the FDs.
let fd_server_config = FdServerConfig {
@@ -197,6 +222,31 @@
ExitCode::from_i32(exit_code.into())
}
+/// Enable fs-verity to output artifacts according to compos.info in the pending directory. Any
+/// error before the completion will just abort, leaving the previous files enabled.
+fn enable_fsverity_to_all() -> Result<()> {
+ let odrefresh_current_dir = Path::new(ODREFRESH_OUTPUT_ROOT_DIR).join(CURRENT_ARTIFACTS_SUBDIR);
+ let pending_dir = Path::new(ODREFRESH_OUTPUT_ROOT_DIR).join(PENDING_ARTIFACTS_SUBDIR);
+ let mut reader =
+ File::open(&pending_dir.join("compos.info")).context("Failed to open compos.info")?;
+ let compos_info = OdsignInfo::parse_from_reader(&mut reader).context("Failed to parse")?;
+
+ for path_str in compos_info.file_hashes.keys() {
+ // Need to rebase the directory on to compos-pending first
+ if let Ok(relpath) = Path::new(path_str).strip_prefix(&odrefresh_current_dir) {
+ let path = pending_dir.join(relpath);
+ let file = File::open(&path).with_context(|| format!("Failed to open {:?}", path))?;
+ // We don't expect error. But when it happens, don't bother handle it here. For
+ // simplicity, just let odsign do the regular check.
+ fsverity::enable(file.as_fd())
+ .with_context(|| format!("Failed to enable fs-verity to {:?}", path))?;
+ } else {
+ warn!("Skip due to unexpected path: {}", path_str);
+ }
+ }
+ Ok(())
+}
+
/// Returns an `OwnedFD` of the directory.
fn open_dir(path: &Path) -> Result<OwnedFd> {
Ok(OwnedFd::from(
diff --git a/compos/composd_cmd/Android.bp b/compos/composd_cmd/Android.bp
index 54b0bad..77caad8 100644
--- a/compos/composd_cmd/Android.bp
+++ b/compos/composd_cmd/Android.bp
@@ -2,8 +2,8 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-rust_binary {
- name: "composd_cmd",
+rust_defaults {
+ name: "composd_cmd_defaults",
srcs: ["composd_cmd.rs"],
edition: "2021",
rustlibs: [
@@ -12,8 +12,14 @@
"libbinder_rs",
"libclap",
"libcompos_common",
+ "libhypervisor_props",
],
prefer_rlib: true,
+}
+
+rust_binary {
+ name: "composd_cmd",
+ defaults: ["composd_cmd_defaults"],
apex_available: [
"com.android.compos",
],
@@ -21,15 +27,6 @@
rust_test {
name: "composd_cmd.test",
- srcs: ["composd_cmd.rs"],
- edition: "2021",
- rustlibs: [
- "android.system.composd-rust",
- "libanyhow",
- "libbinder_rs",
- "libclap",
- "libcompos_common",
- ],
- prefer_rlib: true,
+ defaults: ["composd_cmd_defaults"],
test_suites: ["general-tests"],
}
diff --git a/compos/composd_cmd/composd_cmd.rs b/compos/composd_cmd/composd_cmd.rs
index 19c3720..6d096a1 100644
--- a/compos/composd_cmd/composd_cmd.rs
+++ b/compos/composd_cmd/composd_cmd.rs
@@ -128,6 +128,12 @@
&Strong<dyn ICompilationTaskCallback>,
) -> BinderResult<Strong<dyn ICompilationTask>>,
{
+ if !hypervisor_props::is_any_vm_supported()? {
+ // Give up now, before trying to start composd, or we may end up waiting forever
+ // as it repeatedly starts and then aborts (b/254599807).
+ bail!("Device doesn't support protected or non-protected VMs")
+ }
+
let service = wait_for_interface::<dyn IIsolatedCompilationService>("android.system.composd")
.context("Failed to connect to composd service")?;
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java b/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
index 479ae7f..933ac7a 100644
--- a/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
+++ b/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
@@ -234,6 +234,10 @@
result = IsolatedCompilationMetrics.RESULT_UNEXPECTED_COMPILATION_RESULT;
break;
+ case ICompilationTaskCallback.FailureReason.FailedToEnableFsverity:
+ result = IsolatedCompilationMetrics.RESULT_FAILED_TO_ENABLE_FSVERITY;
+ break;
+
default:
result = IsolatedCompilationMetrics.RESULT_UNKNOWN_FAILURE;
break;
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationMetrics.java b/compos/service/java/com/android/server/compos/IsolatedCompilationMetrics.java
index e333198..f7799a4 100644
--- a/compos/service/java/com/android/server/compos/IsolatedCompilationMetrics.java
+++ b/compos/service/java/com/android/server/compos/IsolatedCompilationMetrics.java
@@ -36,9 +36,17 @@
// TODO(b/218525257): Move the definition of these enums to atoms.proto
@Retention(RetentionPolicy.SOURCE)
- @IntDef({RESULT_UNKNOWN, RESULT_SUCCESS, RESULT_UNKNOWN_FAILURE, RESULT_FAILED_TO_START,
- RESULT_JOB_CANCELED, RESULT_COMPILATION_FAILED, RESULT_UNEXPECTED_COMPILATION_RESULT,
- RESULT_COMPOSD_DIED})
+ @IntDef({
+ RESULT_UNKNOWN,
+ RESULT_SUCCESS,
+ RESULT_UNKNOWN_FAILURE,
+ RESULT_FAILED_TO_START,
+ RESULT_JOB_CANCELED,
+ RESULT_COMPILATION_FAILED,
+ RESULT_UNEXPECTED_COMPILATION_RESULT,
+ RESULT_COMPOSD_DIED,
+ RESULT_FAILED_TO_ENABLE_FSVERITY
+ })
public @interface CompilationResult {}
// Keep this in sync with Result enum in IsolatedCompilationEnded in
@@ -59,6 +67,9 @@
.ISOLATED_COMPILATION_ENDED__COMPILATION_RESULT__RESULT_UNEXPECTED_COMPILATION_RESULT;
public static final int RESULT_COMPOSD_DIED =
ArtStatsLog.ISOLATED_COMPILATION_ENDED__COMPILATION_RESULT__RESULT_COMPOSD_DIED;
+ public static final int RESULT_FAILED_TO_ENABLE_FSVERITY =
+ ArtStatsLog
+ .ISOLATED_COMPILATION_ENDED__COMPILATION_RESULT__RESULT_FAILED_TO_ENABLE_FSVERITY;
@Retention(RetentionPolicy.SOURCE)
@IntDef({SCHEDULING_RESULT_UNKNOWN, SCHEDULING_SUCCESS, SCHEDULING_FAILURE})
diff --git a/compos/verify/verify.rs b/compos/verify/verify.rs
index 528719f..13e9292 100644
--- a/compos/verify/verify.rs
+++ b/compos/verify/verify.rs
@@ -21,7 +21,7 @@
use anyhow::{bail, Context, Result};
use binder::ProcessState;
use clap::{Parser, ValueEnum};
-use compos_common::compos_client::{ComposClient, VmParameters};
+use compos_common::compos_client::{ComposClient, VmCpuTopology, VmParameters};
use compos_common::odrefresh::{
CURRENT_ARTIFACTS_SUBDIR, ODREFRESH_OUTPUT_ROOT_DIR, PENDING_ARTIFACTS_SUBDIR,
TEST_ARTIFACTS_SUBDIR,
@@ -33,7 +33,6 @@
use log::error;
use std::fs::File;
use std::io::Read;
-use std::num::NonZeroU32;
use std::panic;
use std::path::Path;
@@ -116,7 +115,7 @@
&idsig_manifest_apk,
&idsig_manifest_ext_apk,
&VmParameters {
- cpus: Some(NonZeroU32::new(1).unwrap()), // This VM runs very little work at boot
+ cpu_topology: VmCpuTopology::OneCpu, // This VM runs very little work at boot
debug_mode: args.debug,
..Default::default()
},
diff --git a/docs/debug/gdb.md b/docs/debug/gdb.md
new file mode 100644
index 0000000..316faad
--- /dev/null
+++ b/docs/debug/gdb.md
@@ -0,0 +1,46 @@
+# Debugging guest kernels with gdb
+
+Note: this feature is only available on android14-5.15 and newer host kernels.
+
+Starting with Android U it is possible to attach a gdb to the guest kernel, when
+starting a debuggable and non-protected guest VM.
+
+You can do this by passing `--gdb <port>` argument to the `vm run`, `vm run-app`
+and `vm run-microdroid` commands. The `crosvm` will start the gdb server on the
+provided port. It will wait for the gdb client to connect to it before
+proceeding with the VM boot.
+
+Here is an example invocation:
+
+```shell
+adb forward tcp:3456 tcp:3456
+adb shell /apex/com.android.virt/bin/vm run-microdroid --gdb 3456
+```
+
+Then in another shell:
+
+```shell
+gdb vmlinux
+(gdb) target remote :3456
+(gdb) hbreak start_kernel
+(gdb) c
+```
+
+The [kernel documentation](
+https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-debugging.html) has
+some general techniques on how to debug kernel with gdb.
+
+## Obtaining vmlinux for Microdroid kernels
+
+If you are debugging Microdroid kernel that you have built [locally](
+../../microdroid/kernel/README.md), then look for `out/dist/vmlinux` in your
+kernel repository.
+
+If you are debugging Microdroid kernel bundled with the `com.android.virt` APEX,
+then you need to obtain the build ID of this kernel. You can do this by
+checking the prebuilt-info.txt file in the
+`packages/modules/Virtualization/microdroid/kernel/arm64` or
+`packages/modules/Virtualization/microdroid/kernel/x86_64` directories.
+
+Using that build ID you can download the vmlinux from the build server via:
+https://ci.android.com/builds/submitted/${BUILD_ID}/kernel_microdroid_aarch64/latest/vmlinux
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..ebc0ac3
--- /dev/null
+++ b/docs/debug/tracing.md
@@ -0,0 +1,137 @@
+# 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.
+
+The [record_android_trace](
+https://cs.android.com/android/platform/superproject/+/master:external/perfetto/tools/record_android_trace)
+script supports a shortcut to capture all hypervisor events that are known to Perfetto:
+
+```shell
+external/perfetto/tools/record_android_trace hyp -t 15s -b 32mb -o /tmp/hyp.pftrace
+```
+
+Alternatively you can use full trace config to capture hypervisor. Example usage:
+
+```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
+
+Perfetto supports capturing traces on Linux: https://perfetto.dev/docs/quickstart/linux-tracing.
+However, since pKVM hypervisor is only supported on arm64, you will need to cross-compile Perfetto
+binaries for linux-arm64 (unless you have an arm64 workstation).
+
+1. Checkout Perfetto repository: https://perfetto.dev/docs/contributing/getting-started
+2. Follow https://perfetto.dev/docs/contributing/build-instructions#cross-compiling-for-linux-arm-64
+ to compile Perfetto binaries for arm64 architecture.
+3. Copy the tracebox binary to QEMU
+4. Run `tracebox` binary on QEMU to capture traces, it's interface is very similar to the
+`record_android_trace` binary. E.g. to capture all hypervisor events run:
+```shell
+tracebox -t 15s -b 32mb hyp
+```
+
+## Microdroid VM tracing
+
+IMPORTANT: Tracing is only supported for debuggable Microdroid VMs.
+
+### Capturing trace in Microdroid
+
+Starting with Android U, Microdroid contains Perfetto tracing binaries, which makes it possible to
+capture traces inside Microdroid VM using Perfetto stack. The commands used to capture traces on
+Android should work for Microdroid VM as well, with a difference that Perfetto's tracing binaries
+are not enabled in Microdroid by default, so you need to manually start them by setting
+`persist.traced.enable` system property to `1`.
+
+Here is a quick example on how trace Microdroid VM:
+
+1. First start your VM. For this example we are going to use
+`adb shell /apex/com.android.virt/bin/vm run-microdroid`.
+
+2. Set up an adb connection with the running VM:
+```shell
+adb shell forward tcp:9876 vsock:${CID}:5555
+adb connect localhost:9876
+adb -s localhost:9876 root
+```
+Where `${CID}` corresponds to the running Microdroid VM that you want to establish adb connection
+with. List of running VMs can be obtained by running `adb shell /apex/com.android.virt/bin/vm list`.
+Alternatively you can use `vm_shell` utility to connect to a running VM, i.e.: `vm_shell connect`.
+
+3. Start Perfetto daemons and capture trace
+```shell
+adb -s localhost:9876 shell setprop persist.traced.enable 1
+${ANDROID_BULD_TOP}/external/perfetto/tools/record_android_trace \
+ -s localhost:9876 \
+ -o /tmp/microdroid-trace-file.pftrace \
+ -t 10s \
+ -b 32mb \
+ sched/sched_switch task/task_newtask sched/sched_process_exit
+```
+
+If you don't have Android repo checked out, then you can download the record_android_trace script by
+following the following [instructions](
+https://perfetto.dev/docs/quickstart/android-tracing#recording-a-trace-through-the-cmdline)
+
+More documentation on Perfetto's tracing on Android is available here:
+https://perfetto.dev/docs/quickstart/android-tracing
+
+### Capturing Microdroid boot trace
+
+TODO(b/271412868): Stay tuned, more docs are coming soon!
diff --git a/encryptedstore/src/main.rs b/encryptedstore/src/main.rs
index 96c80db..7a41f13 100644
--- a/encryptedstore/src/main.rs
+++ b/encryptedstore/src/main.rs
@@ -95,6 +95,8 @@
.data_device(data_device, dev_size)
.cipher(CipherType::AES256HCTR2)
.key(&key)
+ .opt_param("sector_size:4096")
+ .opt_param("iv_large_sectors")
.build()
.context("Couldn't build the DMCrypt target")?;
let dm = dm::DeviceMapper::new()?;
@@ -125,6 +127,7 @@
let mkfs_options = [
"-j", // Create appropriate sized journal
"-O metadata_csum", // Metadata checksum for filesystem integrity
+ "-b 4096", // block size in the filesystem
];
let mut cmd = Command::new(MK2FS_BIN);
let status = cmd
diff --git a/javalib/api/system-current.txt b/javalib/api/system-current.txt
index b455c85..d9bafa1 100644
--- a/javalib/api/system-current.txt
+++ b/javalib/api/system-current.txt
@@ -57,15 +57,17 @@
public final class VirtualMachineConfig {
method @Nullable public String getApkPath();
- method @NonNull public int getDebugLevel();
+ method public int getCpuTopology();
+ method public int getDebugLevel();
method @IntRange(from=0) public long getEncryptedStorageBytes();
method @IntRange(from=0) public long getMemoryBytes();
- method @IntRange(from=1) public int getNumCpus();
method @Nullable public String getPayloadBinaryName();
method public boolean isCompatibleWith(@NonNull android.system.virtualmachine.VirtualMachineConfig);
method public boolean isEncryptedStorageEnabled();
method public boolean isProtectedVm();
method public boolean isVmOutputCaptured();
+ field public static final int CPU_TOPOLOGY_MATCH_HOST = 1; // 0x1
+ field public static final int CPU_TOPOLOGY_ONE_CPU = 0; // 0x0
field public static final int DEBUG_LEVEL_FULL = 1; // 0x1
field public static final int DEBUG_LEVEL_NONE = 0; // 0x0
}
@@ -74,16 +76,17 @@
ctor public VirtualMachineConfig.Builder(@NonNull android.content.Context);
method @NonNull public android.system.virtualmachine.VirtualMachineConfig build();
method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setApkPath(@NonNull String);
+ method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setCpuTopology(int);
method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setDebugLevel(int);
method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setEncryptedStorageBytes(@IntRange(from=1) long);
method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setMemoryBytes(@IntRange(from=1) long);
- method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setNumCpus(@IntRange(from=1) int);
method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadBinaryName(@NonNull String);
method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setProtectedVm(boolean);
method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setVmOutputCaptured(boolean);
}
- public final class VirtualMachineDescriptor implements android.os.Parcelable {
+ public final class VirtualMachineDescriptor implements java.lang.AutoCloseable android.os.Parcelable {
+ method public void close();
method public int describeContents();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.system.virtualmachine.VirtualMachineDescriptor> CREATOR;
diff --git a/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp b/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
index 9281e73..b3354cc 100644
--- a/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
+++ b/javalib/jni/android_system_virtualmachine_VirtualMachine.cpp
@@ -58,9 +58,10 @@
RpcSessionHandle session;
// We need a thread pool to be able to support linkToDeath, or callbacks
- // (b/268335700). This if a fairly arbitrary number, although it happens to
- // match the default max outgoing threads.
- ARpcSession_setMaxIncomingThreads(session.get(), 10);
+ // (b/268335700). These threads are currently created eagerly, so we don't
+ // want too many. The number 1 is chosen after some discussion, and to match
+ // the server-side default (mMaxThreads on RpcServer).
+ ARpcSession_setMaxIncomingThreads(session.get(), 1);
auto client = ARpcSession_setupPreconnectedClient(session.get(), requestFunc, &args);
return AIBinder_toJavaBinder(env, client);
}
diff --git a/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp b/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp
index bd80880..fbd1fd5 100644
--- a/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp
+++ b/javalib/jni/android_system_virtualmachine_VirtualizationService.cpp
@@ -29,7 +29,7 @@
using namespace android::base;
static constexpr const char VIRTMGR_PATH[] = "/apex/com.android.virt/bin/virtmgr";
-static constexpr size_t VIRTMGR_THREADS = 16;
+static constexpr size_t VIRTMGR_THREADS = 2;
extern "C" JNIEXPORT jint JNICALL
Java_android_system_virtualmachine_VirtualizationService_nativeSpawn(
@@ -83,7 +83,6 @@
ARpcSession_setFileDescriptorTransportMode(session.get(),
ARpcSession_FileDescriptorTransportMode::Unix);
ARpcSession_setMaxIncomingThreads(session.get(), VIRTMGR_THREADS);
- ARpcSession_setMaxOutgoingThreads(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 ba7174e..5f39b1c 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -377,7 +377,7 @@
}
/**
- * Builds a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
+ * Creates a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
* with the given name.
*
* <p>The new virtual machine will be in the same state as the descriptor indicates.
@@ -393,27 +393,29 @@
@NonNull String name,
@NonNull VirtualMachineDescriptor vmDescriptor)
throws VirtualMachineException {
- VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
File vmDir = createVmDir(context, name);
try {
- VirtualMachine vm =
- new VirtualMachine(context, name, config, VirtualizationService.getInstance());
- config.serialize(vm.mConfigFilePath);
- try {
- vm.mInstanceFilePath.createNewFile();
- } catch (IOException e) {
- throw new VirtualMachineException("failed to create instance image", e);
- }
- vm.importInstanceFrom(vmDescriptor.getInstanceImgFd());
-
- if (vmDescriptor.getEncryptedStoreFd() != null) {
+ VirtualMachine vm;
+ try (vmDescriptor) {
+ VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
+ vm = new VirtualMachine(context, name, config, VirtualizationService.getInstance());
+ config.serialize(vm.mConfigFilePath);
try {
- vm.mEncryptedStoreFilePath.createNewFile();
+ vm.mInstanceFilePath.createNewFile();
} catch (IOException e) {
- throw new VirtualMachineException(
- "failed to create encrypted storage image", e);
+ throw new VirtualMachineException("failed to create instance image", e);
}
- vm.importEncryptedStoreFrom(vmDescriptor.getEncryptedStoreFd());
+ vm.importInstanceFrom(vmDescriptor.getInstanceImgFd());
+
+ if (vmDescriptor.getEncryptedStoreFd() != null) {
+ try {
+ vm.mEncryptedStoreFilePath.createNewFile();
+ } catch (IOException e) {
+ throw new VirtualMachineException(
+ "failed to create encrypted storage image", e);
+ }
+ vm.importEncryptedStoreFrom(vmDescriptor.getEncryptedStoreFd());
+ }
}
return vm;
} catch (VirtualMachineException | RuntimeException e) {
@@ -1031,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;
}
}
@@ -1227,8 +1231,7 @@
if (configPath == null) {
return Collections.emptyList();
}
- try {
- ZipFile zipFile = new ZipFile(context.getPackageCodePath());
+ try (ZipFile zipFile = new ZipFile(context.getPackageCodePath())) {
InputStream inputStream =
zipFile.getInputStream(zipFile.getEntry(configPath));
List<String> apkList =
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index cb9bad0..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,10 +60,11 @@
*/
@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.
- private static final int VERSION = 5;
+ private static final int VERSION = 6;
private static final String KEY_VERSION = "version";
private static final String KEY_PACKAGENAME = "packageName";
private static final String KEY_APKPATH = "apkPath";
@@ -69,7 +73,7 @@
private static final String KEY_DEBUGLEVEL = "debugLevel";
private static final String KEY_PROTECTED_VM = "protectedVm";
private static final String KEY_MEMORY_BYTES = "memoryBytes";
- private static final String KEY_NUM_CPUS = "numCpus";
+ private static final String KEY_CPU_TOPOLOGY = "cpuTopology";
private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
@@ -97,6 +101,33 @@
*/
@SystemApi public static final int DEBUG_LEVEL_FULL = 1;
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = "CPU_TOPOLOGY_",
+ value = {
+ CPU_TOPOLOGY_ONE_CPU,
+ CPU_TOPOLOGY_MATCH_HOST,
+ })
+ public @interface CpuTopology {}
+
+ /**
+ * Run VM with 1 vCPU. This is the default option, usually the fastest to boot and consuming the
+ * least amount of resources. Typically the best option for small or ephemeral workloads.
+ *
+ * @hide
+ */
+ @SystemApi public static final int CPU_TOPOLOGY_ONE_CPU = 0;
+
+ /**
+ * Run VM with vCPU topology matching the physical CPU topology of the host. Usually takes
+ * longer to boot and cosumes more resources compared to a single vCPU. Typically a good option
+ * for long-running workloads that benefit from parallel execution.
+ *
+ * @hide
+ */
+ @SystemApi public static final int CPU_TOPOLOGY_MATCH_HOST = 1;
+
/** Name of a package whose primary APK contains the VM payload. */
@Nullable private final String mPackageName;
@@ -116,10 +147,8 @@
*/
private final long mMemoryBytes;
- /**
- * Number of vCPUs in the VM. Defaults to 1 when not specified.
- */
- private final int mNumCpus;
+ /** CPU topology configuration of the VM. */
+ @CpuTopology private final int mCpuTopology;
/**
* Path within the APK to the payload config file that defines software aspects of the VM.
@@ -143,7 +172,7 @@
@DebugLevel int debugLevel,
boolean protectedVm,
long memoryBytes,
- int numCpus,
+ @CpuTopology int cpuTopology,
long encryptedStorageBytes,
boolean vmOutputCaptured) {
// This is only called from Builder.build(); the builder handles parameter validation.
@@ -154,7 +183,7 @@
mDebugLevel = debugLevel;
mProtectedVm = protectedVm;
mMemoryBytes = memoryBytes;
- mNumCpus = numCpus;
+ mCpuTopology = cpuTopology;
mEncryptedStorageBytes = encryptedStorageBytes;
mVmOutputCaptured = vmOutputCaptured;
}
@@ -225,7 +254,7 @@
if (memoryBytes != 0) {
builder.setMemoryBytes(memoryBytes);
}
- builder.setNumCpus(b.getInt(KEY_NUM_CPUS));
+ builder.setCpuTopology(b.getInt(KEY_CPU_TOPOLOGY));
long encryptedStorageBytes = b.getLong(KEY_ENCRYPTED_STORAGE_BYTES);
if (encryptedStorageBytes != 0) {
builder.setEncryptedStorageBytes(encryptedStorageBytes);
@@ -258,7 +287,7 @@
b.putString(KEY_PAYLOADBINARYNAME, mPayloadBinaryName);
b.putInt(KEY_DEBUGLEVEL, mDebugLevel);
b.putBoolean(KEY_PROTECTED_VM, mProtectedVm);
- b.putInt(KEY_NUM_CPUS, mNumCpus);
+ b.putInt(KEY_CPU_TOPOLOGY, mCpuTopology);
if (mMemoryBytes > 0) {
b.putLong(KEY_MEMORY_BYTES, mMemoryBytes);
}
@@ -271,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
*/
@@ -312,7 +340,6 @@
* @hide
*/
@SystemApi
- @NonNull
@DebugLevel
public int getDebugLevel() {
return mDebugLevel;
@@ -341,14 +368,14 @@
}
/**
- * Returns the number of vCPUs that the VM will have.
+ * Returns the CPU topology configuration of the VM.
*
* @hide
*/
@SystemApi
- @IntRange(from = 1)
- public int getNumCpus() {
- return mNumCpus;
+ @CpuTopology
+ public int getCpuTopology() {
+ return mCpuTopology;
}
/**
@@ -397,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
@@ -418,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);
@@ -455,12 +474,58 @@
}
vsConfig.protectedVm = mProtectedVm;
vsConfig.memoryMib = bytesToMebiBytes(mMemoryBytes);
- vsConfig.numCpus = mNumCpus;
+ switch (mCpuTopology) {
+ case CPU_TOPOLOGY_MATCH_HOST:
+ vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.MATCH_HOST;
+ break;
+ default:
+ vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.ONE_CPU;
+ break;
+ }
// Don't allow apps to set task profiles ... at least for now.
vsConfig.taskProfiles = EMPTY_STRING_ARRAY;
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
@@ -486,7 +551,7 @@
private boolean mProtectedVm;
private boolean mProtectedVmSet;
private long mMemoryBytes;
- private int mNumCpus = 1;
+ @CpuTopology private int mCpuTopology = CPU_TOPOLOGY_ONE_CPU;
private long mEncryptedStorageBytes;
private boolean mVmOutputCaptured = false;
@@ -555,14 +620,15 @@
mDebugLevel,
mProtectedVm,
mMemoryBytes,
- mNumCpus,
+ mCpuTopology,
mEncryptedStorageBytes,
mVmOutputCaptured);
}
/**
* 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
*/
@@ -687,25 +753,21 @@
}
/**
- * Sets the number of vCPUs in the VM. Defaults to 1. Cannot be more than the number of real
- * CPUs (as returned by {@link Runtime#availableProcessors}).
+ * Sets the CPU topology configuration of the VM. Defaults to {@link #CPU_TOPOLOGY_ONE_CPU}.
+ *
+ * <p>This determines how many virtual CPUs will be created, and their performance and
+ * scheduling characteristics, such as affinity masks. Topology also has an effect on memory
+ * usage as each vCPU requires additional memory to keep its state.
*
* @hide
*/
@SystemApi
@NonNull
- public Builder setNumCpus(@IntRange(from = 1) int numCpus) {
- int availableCpus = Runtime.getRuntime().availableProcessors();
- if (numCpus < 1 || numCpus > availableCpus) {
- throw new IllegalArgumentException(
- "Number of vCPUs ("
- + numCpus
- + ") is out of "
- + "range [1, "
- + availableCpus
- + "]");
+ public Builder setCpuTopology(@CpuTopology int cpuTopology) {
+ if (cpuTopology != CPU_TOPOLOGY_ONE_CPU && cpuTopology != CPU_TOPOLOGY_MATCH_HOST) {
+ throw new IllegalArgumentException("Invalid cpuTopology: " + cpuTopology);
}
- mNumCpus = numCpus;
+ mCpuTopology = cpuTopology;
return this;
}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
index 483779a..710925d 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -25,6 +25,8 @@
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
+import java.io.IOException;
+
/**
* A VM descriptor that captures the state of a Virtual Machine.
*
@@ -35,7 +37,8 @@
* @hide
*/
@SystemApi
-public final class VirtualMachineDescriptor implements Parcelable {
+public final class VirtualMachineDescriptor implements Parcelable, AutoCloseable {
+ private volatile boolean mClosed = false;
@NonNull private final ParcelFileDescriptor mConfigFd;
@NonNull private final ParcelFileDescriptor mInstanceImgFd;
// File descriptor of the image backing the encrypted storage - Will be null if encrypted
@@ -49,9 +52,10 @@
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
- mConfigFd.writeToParcel(out, flags);
- mInstanceImgFd.writeToParcel(out, flags);
- if (mEncryptedStoreFd != null) mEncryptedStoreFd.writeToParcel(out, flags);
+ checkNotClosed();
+ out.writeParcelable(mConfigFd, flags);
+ out.writeParcelable(mInstanceImgFd, flags);
+ out.writeParcelable(mEncryptedStoreFd, flags);
}
@NonNull
@@ -71,6 +75,7 @@
*/
@NonNull
ParcelFileDescriptor getConfigFd() {
+ checkNotClosed();
return mConfigFd;
}
@@ -79,6 +84,7 @@
*/
@NonNull
ParcelFileDescriptor getInstanceImgFd() {
+ checkNotClosed();
return mInstanceImgFd;
}
@@ -88,6 +94,7 @@
*/
@Nullable
ParcelFileDescriptor getEncryptedStoreFd() {
+ checkNotClosed();
return mEncryptedStoreFd;
}
@@ -95,14 +102,42 @@
@NonNull ParcelFileDescriptor configFd,
@NonNull ParcelFileDescriptor instanceImgFd,
@Nullable ParcelFileDescriptor encryptedStoreFd) {
- mConfigFd = configFd;
- mInstanceImgFd = instanceImgFd;
+ mConfigFd = requireNonNull(configFd);
+ mInstanceImgFd = requireNonNull(instanceImgFd);
mEncryptedStoreFd = encryptedStoreFd;
}
private VirtualMachineDescriptor(Parcel in) {
- mConfigFd = requireNonNull(in.readFileDescriptor());
- mInstanceImgFd = requireNonNull(in.readFileDescriptor());
- mEncryptedStoreFd = in.readFileDescriptor();
+ mConfigFd = requireNonNull(readParcelFileDescriptor(in));
+ mInstanceImgFd = requireNonNull(readParcelFileDescriptor(in));
+ mEncryptedStoreFd = readParcelFileDescriptor(in);
+ }
+
+ private ParcelFileDescriptor readParcelFileDescriptor(Parcel in) {
+ return in.readParcelable(
+ ParcelFileDescriptor.class.getClassLoader(), ParcelFileDescriptor.class);
+ }
+
+ /**
+ * Release any resources held by this descriptor. Calling {@code close} on an already-closed
+ * descriptor has no effect.
+ */
+ @Override
+ public void close() {
+ mClosed = true;
+ // Let the compiler do the work: close everything, throw if any of them fail, skipping null.
+ try (mConfigFd;
+ mInstanceImgFd;
+ mEncryptedStoreFd) {
+ } catch (IOException ignored) {
+ // PFD already swallows exceptions from closing the fd. There's no reason to propagate
+ // this to the caller.
+ }
+ }
+
+ private void checkNotClosed() {
+ if (mClosed) {
+ throw new IllegalStateException("Descriptor has been closed");
+ }
}
}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index 7773cb5..b7ea22c 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -191,11 +191,13 @@
* Imports a virtual machine from an {@link VirtualMachineDescriptor} object and associates it
* with the given name.
*
- * <p>The new virtual machine will be in the same state as the descriptor indicates.
+ * <p>The new virtual machine will be in the same state as the descriptor indicates. The
+ * descriptor is automatically closed and cannot be used again.
*
* <p>NOTE: This method may block and should not be called on the main thread.
*
- * @throws VirtualMachineException if the VM cannot be imported.
+ * @throws VirtualMachineException if the VM cannot be imported or the {@code
+ * VirtualMachineDescriptor} has already been closed.
* @hide
*/
@NonNull
diff --git a/libs/apkverify/Android.bp b/libs/apkverify/Android.bp
index e556842..83dbff6 100644
--- a/libs/apkverify/Android.bp
+++ b/libs/apkverify/Android.bp
@@ -45,7 +45,9 @@
edition: "2021",
test_suites: ["general-tests"],
rustlibs: [
+ "libandroid_logger",
"libapkverify",
+ "liblog_rust",
"libzip",
],
data: ["tests/data/*"],
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/v3.rs b/libs/apkverify/src/v3.rs
index e1b728d..6082422 100644
--- a/libs/apkverify/src/v3.rs
+++ b/libs/apkverify/src/v3.rs
@@ -24,7 +24,7 @@
use openssl::x509::X509;
use std::fs::File;
use std::io::{Read, Seek};
-use std::ops::Range;
+use std::ops::RangeInclusive;
use std::path::Path;
use crate::algorithms::SignatureAlgorithmID;
@@ -33,11 +33,9 @@
pub const APK_SIGNATURE_SCHEME_V3_BLOCK_ID: u32 = 0xf05368c0;
-// TODO(b/190343842): get "ro.build.version.sdk"
-const SDK_INT: u32 = 31;
-
type Signers = LengthPrefixed<Vec<LengthPrefixed<Signer>>>;
+#[derive(Debug)]
pub(crate) struct Signer {
signed_data: LengthPrefixed<Bytes>, // not verified yet
min_sdk: u32,
@@ -47,8 +45,8 @@
}
impl Signer {
- fn sdk_range(&self) -> Range<u32> {
- self.min_sdk..self.max_sdk
+ fn sdk_range(&self) -> RangeInclusive<u32> {
+ self.min_sdk..=self.max_sdk
}
}
@@ -62,8 +60,8 @@
}
impl SignedData {
- fn sdk_range(&self) -> Range<u32> {
- self.min_sdk..self.max_sdk
+ fn sdk_range(&self) -> RangeInclusive<u32> {
+ self.min_sdk..=self.max_sdk
}
fn find_digest_by_algorithm(&self, algorithm_id: SignatureAlgorithmID) -> Result<&Digest> {
@@ -92,32 +90,30 @@
/// Verifies APK Signature Scheme v3 signatures of the provided APK and returns the public key
/// associated with the signer in DER format.
-pub fn verify<P: AsRef<Path>>(apk_path: P) -> Result<Box<[u8]>> {
+pub fn verify<P: AsRef<Path>>(apk_path: P, current_sdk: u32) -> Result<Box<[u8]>> {
let apk = File::open(apk_path.as_ref())?;
- let (signer, mut sections) = extract_signer_and_apk_sections(apk)?;
+ let (signer, mut sections) = extract_signer_and_apk_sections(apk, current_sdk)?;
signer.verify(&mut sections)
}
/// Gets the public key (in DER format) that was used to sign the given APK/APEX file
-pub fn get_public_key_der<P: AsRef<Path>>(apk_path: P) -> Result<Box<[u8]>> {
+pub fn get_public_key_der<P: AsRef<Path>>(apk_path: P, current_sdk: u32) -> Result<Box<[u8]>> {
let apk = File::open(apk_path.as_ref())?;
- let (signer, _) = extract_signer_and_apk_sections(apk)?;
+ let (signer, _) = extract_signer_and_apk_sections(apk, current_sdk)?;
Ok(signer.public_key.public_key_to_der()?.into_boxed_slice())
}
pub(crate) fn extract_signer_and_apk_sections<R: Read + Seek>(
apk: R,
+ current_sdk: u32,
) -> Result<(Signer, ApkSections<R>)> {
let mut sections = ApkSections::new(apk)?;
let mut block = sections.find_signature(APK_SIGNATURE_SCHEME_V3_BLOCK_ID).context(
"Fallback to v2 when v3 block not found is not yet implemented.", // b/197052981
)?;
- let mut supported = block
- .read::<Signers>()?
- .into_inner()
- .into_iter()
- .filter(|s| s.sdk_range().contains(&SDK_INT))
- .collect::<Vec<_>>();
+ let signers = block.read::<Signers>()?.into_inner();
+ let mut supported =
+ signers.into_iter().filter(|s| s.sdk_range().contains(¤t_sdk)).collect::<Vec<_>>();
ensure!(
supported.len() == 1,
"APK Signature Scheme V3 only supports one signer: {} signers found.",
diff --git a/libs/apkverify/src/v4.rs b/libs/apkverify/src/v4.rs
index 94abf99..045f4af 100644
--- a/libs/apkverify/src/v4.rs
+++ b/libs/apkverify/src/v4.rs
@@ -37,9 +37,10 @@
/// [apk_digest]: https://source.android.com/docs/security/apksigning/v4#apk-digest
pub fn get_apk_digest<R: Read + Seek>(
apk: R,
+ current_sdk: u32,
verify: bool,
) -> Result<(SignatureAlgorithmID, Box<[u8]>)> {
- let (signer, mut sections) = extract_signer_and_apk_sections(apk)?;
+ let (signer, mut sections) = extract_signer_and_apk_sections(apk, current_sdk)?;
let strongest_algorithm_id = signer
.strongest_signature()?
.signature_algorithm_id
@@ -104,9 +105,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 +119,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> {
@@ -153,6 +149,7 @@
/// function OOMing.
pub fn create(
mut apk: &mut R,
+ current_sdk: u32,
block_size: usize,
salt: &[u8],
algorithm: HashAlgorithm,
@@ -180,7 +177,8 @@
ret.hashing_info.log2_blocksize = log2(block_size);
apk.seek(SeekFrom::Start(start))?;
- let (signature_algorithm_id, apk_digest) = get_apk_digest(apk, /*verify=*/ false)?;
+ let (signature_algorithm_id, apk_digest) =
+ get_apk_digest(apk, current_sdk, /*verify=*/ false)?;
ret.signing_info.signature_algorithm_id = signature_algorithm_id;
ret.signing_info.apk_digest = apk_digest;
// TODO(jiyong): add a signature to the signing_info struct
@@ -367,8 +365,9 @@
#[test]
fn digest_from_apk() {
let mut input = Cursor::new(include_bytes!("../tests/data/v4-digest-v3-Sha256withEC.apk"));
+ let current_sdk = 31;
let mut created =
- V4Signature::create(&mut input, 4096, &[], HashAlgorithm::SHA256).unwrap();
+ V4Signature::create(&mut input, current_sdk, 4096, &[], HashAlgorithm::SHA256).unwrap();
let mut golden = V4Signature::from_idsig_path(format!("{}.idsig", TEST_APK_PATH)).unwrap();
diff --git a/libs/apkverify/src/ziputil.rs b/libs/apkverify/src/ziputil.rs
index cc8bc58..5e513a7 100644
--- a/libs/apkverify/src/ziputil.rs
+++ b/libs/apkverify/src/ziputil.rs
@@ -18,9 +18,12 @@
use anyhow::{ensure, Result};
use bytes::{Buf, BufMut};
-use std::io::{Read, Seek, SeekFrom};
+use std::io::{Read, Seek};
use zip::ZipArchive;
+#[cfg(test)]
+use std::io::SeekFrom;
+
const EOCD_SIZE_WITHOUT_COMMENT: usize = 22;
const EOCD_CENTRAL_DIRECTORY_SIZE_FIELD_OFFSET: usize = 12;
const EOCD_CENTRAL_DIRECTORY_OFFSET_FIELD_OFFSET: usize = 16;
@@ -45,7 +48,7 @@
// retrieve reader back
reader = archive.into_inner();
// the current position should point EOCD offset
- let eocd_offset = reader.seek(SeekFrom::Current(0))? as u32;
+ let eocd_offset = reader.stream_position()? as u32;
let mut eocd = vec![0u8; eocd_size];
reader.read_exact(&mut eocd)?;
ensure!(
diff --git a/libs/apkverify/tests/apkverify_test.rs b/libs/apkverify/tests/apkverify_test.rs
index baf7c42..52e1da4 100644
--- a/libs/apkverify/tests/apkverify_test.rs
+++ b/libs/apkverify/tests/apkverify_test.rs
@@ -17,16 +17,30 @@
use apkverify::{
get_apk_digest, get_public_key_der, testing::assert_contains, verify, SignatureAlgorithmID,
};
+use log::info;
use std::{fs, matches, path::Path};
const KEY_NAMES_DSA: &[&str] = &["1024", "2048", "3072"];
const KEY_NAMES_ECDSA: &[&str] = &["p256", "p384", "p521"];
const KEY_NAMES_RSA: &[&str] = &["1024", "2048", "3072", "4096", "8192", "16384"];
+const SDK_INT: u32 = 31;
+
+/// Make sure any logging from the code under test ends up in logcat.
+fn setup() {
+ android_logger::init_once(
+ android_logger::Config::default()
+ .with_tag("apkverify_test")
+ .with_min_level(log::Level::Info),
+ );
+ info!("Test starting");
+}
+
#[test]
fn test_verify_truncated_cd() {
+ setup();
use zip::result::ZipError;
- let res = verify("tests/data/v2-only-truncated-cd.apk");
+ let res = verify("tests/data/v2-only-truncated-cd.apk", SDK_INT);
// TODO(b/190343842): consider making a helper for err assertion
assert!(matches!(
res.unwrap_err().root_cause().downcast_ref::<ZipError>().unwrap(),
@@ -36,13 +50,15 @@
#[test]
fn apex_signed_with_v3_rsa_pkcs1_sha512_is_valid() {
+ setup();
validate_apk("tests/data/test.apex", SignatureAlgorithmID::RsaPkcs1V15WithSha512);
}
#[test]
fn apks_signed_with_v3_dsa_sha256_are_not_supported() {
+ setup();
for key_name in KEY_NAMES_DSA.iter() {
- let res = verify(format!("tests/data/v3-only-with-dsa-sha256-{}.apk", key_name));
+ let res = verify(format!("tests/data/v3-only-with-dsa-sha256-{}.apk", key_name), SDK_INT);
assert!(res.is_err(), "DSA algorithm is not supported for verification. See b/197052981.");
assert_contains(&res.unwrap_err().to_string(), "No supported APK signatures found");
}
@@ -50,6 +66,7 @@
#[test]
fn apks_signed_with_v3_ecdsa_sha256_are_valid() {
+ setup();
for key_name in KEY_NAMES_ECDSA.iter() {
validate_apk(
format!("tests/data/v3-only-with-ecdsa-sha256-{}.apk", key_name),
@@ -60,6 +77,7 @@
#[test]
fn apks_signed_with_v3_ecdsa_sha512_are_valid() {
+ setup();
for key_name in KEY_NAMES_ECDSA.iter() {
validate_apk(
format!("tests/data/v3-only-with-ecdsa-sha512-{}.apk", key_name),
@@ -70,6 +88,7 @@
#[test]
fn apks_signed_with_v3_rsa_pkcs1_sha256_are_valid() {
+ setup();
for key_name in KEY_NAMES_RSA.iter() {
validate_apk(
format!("tests/data/v3-only-with-rsa-pkcs1-sha256-{}.apk", key_name),
@@ -80,6 +99,7 @@
#[test]
fn apks_signed_with_v3_rsa_pkcs1_sha512_are_valid() {
+ setup();
for key_name in KEY_NAMES_RSA.iter() {
validate_apk(
format!("tests/data/v3-only-with-rsa-pkcs1-sha512-{}.apk", key_name),
@@ -89,13 +109,35 @@
}
#[test]
+fn test_verify_v3_sig_min_max_sdk() {
+ setup();
+ // The Signer for this APK has min_sdk=24, max_sdk=32.
+ let path = "tests/data/v31-rsa-2048_2-tgt-33-1-tgt-28.apk";
+
+ let res = verify(path, 23);
+ assert!(res.is_err());
+ assert_contains(&res.unwrap_err().to_string(), "0 signers found");
+
+ let res = verify(path, 24);
+ assert!(res.is_ok());
+
+ let res = verify(path, 32);
+ assert!(res.is_ok());
+
+ let res = verify(path, 33);
+ assert!(res.is_err());
+ assert_contains(&res.unwrap_err().to_string(), "0 signers found");
+}
+
+#[test]
fn test_verify_v3_sig_does_not_verify() {
+ setup();
let path_list = [
"tests/data/v3-only-with-ecdsa-sha512-p521-sig-does-not-verify.apk",
"tests/data/v3-only-with-rsa-pkcs1-sha256-3072-sig-does-not-verify.apk",
];
for path in path_list.iter() {
- let res = verify(path);
+ let res = verify(path, SDK_INT);
assert!(res.is_err());
assert_contains(&res.unwrap_err().to_string(), "Signature is invalid");
}
@@ -103,22 +145,28 @@
#[test]
fn test_verify_v3_digest_mismatch() {
- let res = verify("tests/data/v3-only-with-rsa-pkcs1-sha512-8192-digest-mismatch.apk");
+ setup();
+ let res = verify("tests/data/v3-only-with-rsa-pkcs1-sha512-8192-digest-mismatch.apk", SDK_INT);
assert!(res.is_err());
assert_contains(&res.unwrap_err().to_string(), "Digest mismatch");
}
#[test]
fn test_verify_v3_wrong_apk_sig_block_magic() {
- let res = verify("tests/data/v3-only-with-ecdsa-sha512-p384-wrong-apk-sig-block-magic.apk");
+ setup();
+ let res =
+ verify("tests/data/v3-only-with-ecdsa-sha512-p384-wrong-apk-sig-block-magic.apk", SDK_INT);
assert!(res.is_err());
assert_contains(&res.unwrap_err().to_string(), "No APK Signing Block");
}
#[test]
fn test_verify_v3_apk_sig_block_size_mismatch() {
- let res =
- verify("tests/data/v3-only-with-rsa-pkcs1-sha512-4096-apk-sig-block-size-mismatch.apk");
+ setup();
+ let res = verify(
+ "tests/data/v3-only-with-rsa-pkcs1-sha512-4096-apk-sig-block-size-mismatch.apk",
+ SDK_INT,
+ );
assert!(res.is_err());
assert_contains(
&res.unwrap_err().to_string(),
@@ -128,35 +176,40 @@
#[test]
fn test_verify_v3_cert_and_public_key_mismatch() {
- let res = verify("tests/data/v3-only-cert-and-public-key-mismatch.apk");
+ setup();
+ let res = verify("tests/data/v3-only-cert-and-public-key-mismatch.apk", SDK_INT);
assert!(res.is_err());
assert_contains(&res.unwrap_err().to_string(), "Public key mismatch");
}
#[test]
fn test_verify_v3_empty() {
- let res = verify("tests/data/v3-only-empty.apk");
+ setup();
+ let res = verify("tests/data/v3-only-empty.apk", SDK_INT);
assert!(res.is_err());
assert_contains(&res.unwrap_err().to_string(), "APK too small for APK Signing Block");
}
#[test]
fn test_verify_v3_no_certs_in_sig() {
- let res = verify("tests/data/v3-only-no-certs-in-sig.apk");
+ setup();
+ let res = verify("tests/data/v3-only-no-certs-in-sig.apk", SDK_INT);
assert!(res.is_err());
assert_contains(&res.unwrap_err().to_string(), "No certificates listed");
}
#[test]
fn test_verify_v3_no_supported_sig_algs() {
- let res = verify("tests/data/v3-only-no-supported-sig-algs.apk");
+ setup();
+ let res = verify("tests/data/v3-only-no-supported-sig-algs.apk", SDK_INT);
assert!(res.is_err());
assert_contains(&res.unwrap_err().to_string(), "No supported APK signatures found");
}
#[test]
fn test_verify_v3_signatures_and_digests_block_mismatch() {
- let res = verify("tests/data/v3-only-signatures-and-digests-block-mismatch.apk");
+ setup();
+ let res = verify("tests/data/v3-only-signatures-and-digests-block-mismatch.apk", SDK_INT);
assert!(res.is_err());
assert_contains(
&res.unwrap_err().to_string(),
@@ -166,6 +219,7 @@
#[test]
fn apk_signed_with_v3_unknown_additional_attr_is_valid() {
+ setup();
validate_apk(
"tests/data/v3-only-unknown-additional-attr.apk",
SignatureAlgorithmID::RsaPkcs1V15WithSha256,
@@ -174,6 +228,7 @@
#[test]
fn apk_signed_with_v3_unknown_pair_in_apk_sig_block_is_valid() {
+ setup();
validate_apk(
"tests/data/v3-only-unknown-pair-in-apk-sig-block.apk",
SignatureAlgorithmID::RsaPkcs1V15WithSha256,
@@ -182,6 +237,7 @@
#[test]
fn apk_signed_with_v3_ignorable_unsupported_sig_algs_is_valid() {
+ setup();
validate_apk(
"tests/data/v3-only-with-ignorable-unsupported-sig-algs.apk",
SignatureAlgorithmID::RsaPkcs1V15WithSha256,
@@ -190,6 +246,7 @@
#[test]
fn apk_signed_with_v3_stamp_is_valid() {
+ setup();
validate_apk("tests/data/v3-only-with-stamp.apk", SignatureAlgorithmID::EcdsaWithSha256);
}
@@ -203,14 +260,14 @@
/// * public key extracted from apk without verification
/// * expected public key from the corresponding .der file
fn validate_apk_public_key<P: AsRef<Path>>(apk_path: P) {
- let public_key_from_verification = verify(&apk_path);
+ let public_key_from_verification = verify(&apk_path, SDK_INT);
let public_key_from_verification =
public_key_from_verification.expect("Error in verification result");
let expected_public_key_path = format!("{}.der", apk_path.as_ref().to_str().unwrap());
assert_bytes_eq_to_data_in_file(&public_key_from_verification, expected_public_key_path);
- let public_key_from_apk = get_public_key_der(&apk_path);
+ let public_key_from_apk = get_public_key_der(&apk_path, SDK_INT);
let public_key_from_apk =
public_key_from_apk.expect("Error when extracting public key from apk");
assert_eq!(
@@ -226,15 +283,17 @@
fn validate_apk_digest<P: AsRef<Path>>(apk_path: P, expected_algorithm_id: SignatureAlgorithmID) {
let apk = fs::File::open(&apk_path).expect("Unabled to open apk file");
- let (verified_algorithm_id, verified_digest) = get_apk_digest(&apk, /*verify=*/ true)
- .expect("Error when extracting apk digest with verification.");
+ let (verified_algorithm_id, verified_digest) =
+ get_apk_digest(&apk, SDK_INT, /*verify=*/ true)
+ .expect("Error when extracting apk digest with verification.");
assert_eq!(expected_algorithm_id, verified_algorithm_id);
let expected_digest_path = format!("{}.apk_digest", apk_path.as_ref().to_str().unwrap());
assert_bytes_eq_to_data_in_file(&verified_digest, expected_digest_path);
- let (unverified_algorithm_id, unverified_digest) = get_apk_digest(&apk, /*verify=*/ false)
- .expect("Error when extracting apk digest without verification.");
+ let (unverified_algorithm_id, unverified_digest) =
+ get_apk_digest(&apk, SDK_INT, /*verify=*/ false)
+ .expect("Error when extracting apk digest without verification.");
assert_eq!(expected_algorithm_id, unverified_algorithm_id);
assert_eq!(verified_digest, unverified_digest);
}
diff --git a/libs/apkverify/tests/data/v31-rsa-2048_2-tgt-33-1-tgt-28.apk b/libs/apkverify/tests/data/v31-rsa-2048_2-tgt-33-1-tgt-28.apk
new file mode 100644
index 0000000..aeaec33
--- /dev/null
+++ b/libs/apkverify/tests/data/v31-rsa-2048_2-tgt-33-1-tgt-28.apk
Binary files differ
diff --git a/libs/avb/Android.bp b/libs/avb/Android.bp
index c173f1c..7bfea3f 100644
--- a/libs/avb/Android.bp
+++ b/libs/avb/Android.bp
@@ -2,9 +2,8 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-rust_bindgen {
- name: "libavb_bindgen",
- host_supported: true,
+rust_defaults {
+ name: "libavb_bindgen.defaults",
wrapper_src: "bindgen/avb.h",
crate_name: "avb_bindgen",
edition: "2021",
@@ -19,13 +18,28 @@
"--raw-line=#![no_std]",
"--ctypes-prefix=core::ffi",
],
+ cflags: ["-DBORINGSSL_NO_CXX"],
+}
+
+rust_bindgen {
+ name: "libavb_bindgen",
+ defaults: ["libavb_bindgen.defaults"],
+ host_supported: true,
static_libs: [
- "libavb_non_debug",
+ "libavb",
],
shared_libs: [
"libcrypto",
],
- cflags: ["-DBORINGSSL_NO_CXX"],
+}
+
+rust_bindgen {
+ name: "libavb_bindgen_nostd",
+ defaults: ["libavb_bindgen.defaults"],
+ static_libs: [
+ "libavb_baremetal",
+ "libcrypto_baremetal",
+ ],
}
rust_test {
diff --git a/libs/devicemapper/src/crypt.rs b/libs/devicemapper/src/crypt.rs
index b2e677a..8281b34 100644
--- a/libs/devicemapper/src/crypt.rs
+++ b/libs/devicemapper/src/crypt.rs
@@ -76,7 +76,7 @@
device_path: Option<&'a Path>,
offset: u64,
device_size: u64,
- // TODO(b/238179332) Extend this to include opt_params, in particular 'integrity'
+ opt_params: Vec<&'a str>,
}
impl<'a> Default for DmCryptTargetBuilder<'a> {
@@ -88,6 +88,7 @@
device_path: None,
offset: 0,
device_size: 0,
+ opt_params: Vec::new(),
}
}
}
@@ -124,6 +125,12 @@
self
}
+ /// Add additional optional parameter
+ pub fn opt_param(&mut self, param: &'a str) -> &mut Self {
+ self.opt_params.push(param);
+ self
+ }
+
/// Constructs a `DmCryptTarget`.
pub fn build(&self) -> Result<DmCryptTarget> {
// The `DmCryptTarget` struct actually is a flattened data consisting of a header and
@@ -154,6 +161,7 @@
write!(&mut body, "{} ", self.iv_offset)?;
write!(&mut body, "{} ", device_path)?;
write!(&mut body, "{} ", self.offset)?;
+ write!(&mut body, "{} {} ", self.opt_params.len(), self.opt_params.join(" "))?;
write!(&mut body, "\0")?; // null terminator
let size = size_of::<DmTargetSpec>() + body.len();
diff --git a/libs/dice/Android.bp b/libs/dice/Android.bp
deleted file mode 100644
index 71cf0f1..0000000
--- a/libs/dice/Android.bp
+++ /dev/null
@@ -1,24 +0,0 @@
-package {
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-rust_library_rlib {
- name: "libdice_nostd",
- crate_name: "dice",
- srcs: ["src/lib.rs"],
- edition: "2021",
- no_stdlibs: true,
- prefer_rlib: true,
- stdlibs: ["libcore.rust_sysroot"],
- rustlibs: [
- "libdiced_open_dice_nostd",
- "libopen_dice_cbor_bindgen_nostd",
- "libopen_dice_bcc_bindgen_nostd",
- ],
- whole_static_libs: [
- "libopen_dice_bcc",
- "libopen_dice_cbor",
- "libcrypto_baremetal",
- ],
- apex_available: ["com.android.virt"],
-}
diff --git a/libs/dice/src/bcc.rs b/libs/dice/src/bcc.rs
deleted file mode 100644
index a7ef882..0000000
--- a/libs/dice/src/bcc.rs
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-//! Wrapper around dice/android/bcc.h.
-
-use core::mem;
-use core::ptr;
-
-use open_dice_bcc_bindgen::BccHandoverMainFlow;
-use open_dice_bcc_bindgen::BccHandoverParse;
-
-use crate::check_result;
-use crate::Cdi;
-use crate::DiceError;
-use crate::InputValues;
-use crate::Result;
-
-/// Boot Chain Certificate handover format combining the BCC and CDIs in a single CBOR object.
-#[derive(Clone, Debug)]
-pub struct Handover<'a> {
- buffer: &'a [u8],
- /// Attestation CDI.
- pub cdi_attest: &'a Cdi,
- /// Sealing CDI.
- pub cdi_seal: &'a Cdi,
- /// Boot Chain Certificate (optional).
- pub bcc: Option<&'a [u8]>,
-}
-
-impl<'a> Handover<'a> {
- /// Validates and extracts the fields of a BCC handover buffer.
- pub fn new(buffer: &'a [u8]) -> Result<Self> {
- let mut cdi_attest: *const u8 = ptr::null();
- let mut cdi_seal: *const u8 = ptr::null();
- let mut bcc: *const u8 = ptr::null();
- let mut bcc_size: usize = 0;
-
- // SAFETY - The buffer is only read and never stored and the returned pointers should all
- // point within the address range of the buffer or be NULL.
- check_result(unsafe {
- BccHandoverParse(
- buffer.as_ptr(),
- buffer.len(),
- &mut cdi_attest as *mut *const u8,
- &mut cdi_seal as *mut *const u8,
- &mut bcc as *mut *const u8,
- &mut bcc_size as *mut usize,
- )
- })?;
-
- let cdi_attest = {
- let i = index_from_ptr(buffer, cdi_attest).ok_or(DiceError::PlatformError)?;
- let s = buffer.get(i..(i + mem::size_of::<Cdi>())).ok_or(DiceError::PlatformError)?;
- s.try_into().map_err(|_| DiceError::PlatformError)?
- };
- let cdi_seal = {
- let i = index_from_ptr(buffer, cdi_seal).ok_or(DiceError::PlatformError)?;
- let s = buffer.get(i..(i + mem::size_of::<Cdi>())).ok_or(DiceError::PlatformError)?;
- s.try_into().map_err(|_| DiceError::PlatformError)?
- };
- let bcc = if bcc.is_null() {
- None
- } else {
- let i = index_from_ptr(buffer, bcc).ok_or(DiceError::PlatformError)?;
- Some(buffer.get(i..(i + bcc_size)).ok_or(DiceError::PlatformError)?)
- };
-
- Ok(Self { buffer, cdi_attest, cdi_seal, bcc })
- }
-
- /// Executes the main BCC handover flow.
- pub fn main_flow(&self, input_values: &InputValues, buffer: &mut [u8]) -> Result<usize> {
- let context = ptr::null_mut();
- let mut size: usize = 0;
- // SAFETY - The function only reads `self.buffer`, writes to `buffer` within its bounds,
- // reads `input_values` as a constant input and doesn't store any pointer.
- check_result(unsafe {
- BccHandoverMainFlow(
- context,
- self.buffer.as_ptr(),
- self.buffer.len(),
- input_values.as_ptr(),
- buffer.len(),
- buffer.as_mut_ptr(),
- &mut size as *mut usize,
- )
- })?;
-
- Ok(size)
- }
-}
-
-fn index_from_ptr(slice: &[u8], pointer: *const u8) -> Option<usize> {
- if slice.as_ptr_range().contains(&pointer) {
- (pointer as usize).checked_sub(slice.as_ptr() as usize)
- } else {
- None
- }
-}
diff --git a/libs/fdtpci/src/lib.rs b/libs/fdtpci/src/lib.rs
index e32e16d..96d98d6 100644
--- a/libs/fdtpci/src/lib.rs
+++ b/libs/fdtpci/src/lib.rs
@@ -197,24 +197,32 @@
Ok(memory_address..memory_address + memory_size)
}
+/// Encodes memory flags of a PCI range
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-struct PciMemoryFlags(u32);
+pub struct PciMemoryFlags(pub u32);
impl PciMemoryFlags {
+ /// Returns whether this PCI range is prefetchable
pub fn prefetchable(self) -> bool {
self.0 & 0x80000000 != 0
}
+ /// Returns the type of this PCI range
pub fn range_type(self) -> PciRangeType {
PciRangeType::from((self.0 & 0x3000000) >> 24)
}
}
+/// Type of a PCI range
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-enum PciRangeType {
+pub enum PciRangeType {
+ /// Range represents the PCI configuration space
ConfigurationSpace,
+ /// Range is on IO space
IoSpace,
+ /// Range is on 32-bit MMIO space
Memory32,
+ /// Range is on 64-bit MMIO space
Memory64,
}
diff --git a/libs/hypervisor_props/Android.bp b/libs/hypervisor_props/Android.bp
new file mode 100644
index 0000000..af08b01
--- /dev/null
+++ b/libs/hypervisor_props/Android.bp
@@ -0,0 +1,18 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_library {
+ name: "libhypervisor_props",
+ crate_name: "hypervisor_props",
+ srcs: ["src/lib.rs"],
+ edition: "2021",
+ rustlibs: [
+ "libanyhow",
+ "librustutils",
+ ],
+ apex_available: [
+ "com.android.compos",
+ "com.android.virt",
+ ],
+}
diff --git a/libs/hypervisor_props/src/lib.rs b/libs/hypervisor_props/src/lib.rs
new file mode 100644
index 0000000..120a48c
--- /dev/null
+++ b/libs/hypervisor_props/src/lib.rs
@@ -0,0 +1,40 @@
+// 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.
+
+//! Access to hypervisor capabilities via system properties set by the bootloader.
+
+use anyhow::{Error, Result};
+use rustutils::system_properties;
+
+/// Returns whether there is a hypervisor present that supports non-protected VMs.
+pub fn is_vm_supported() -> Result<bool> {
+ system_properties::read_bool("ro.boot.hypervisor.vm.supported", false).map_err(Error::new)
+}
+
+/// Returns whether there is a hypervisor present that supports protected VMs.
+pub fn is_protected_vm_supported() -> Result<bool> {
+ system_properties::read_bool("ro.boot.hypervisor.protected_vm.supported", false)
+ .map_err(Error::new)
+}
+
+/// Returns whether there is a hypervisor present that supports any sort of VM, either protected
+/// or non-protected.
+pub fn is_any_vm_supported() -> Result<bool> {
+ is_vm_supported().and_then(|ok| if ok { Ok(true) } else { is_protected_vm_supported() })
+}
+
+/// Returns the version of the hypervisor, if there is one.
+pub fn version() -> Result<Option<String>> {
+ system_properties::read("ro.boot.hypervisor.version").map_err(Error::new)
+}
diff --git a/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index 29d7abe..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)
@@ -614,7 +637,8 @@
fdt_err_expect_zero(ret)
}
- fn as_ptr(&self) -> *const c_void {
+ /// Return a shared pointer to the device tree.
+ pub fn as_ptr(&self) -> *const c_void {
self as *const _ as *const c_void
}
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 9264692..0abaf79 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -51,6 +51,7 @@
deps: [
"init_second_stage",
"microdroid_build_prop",
+ "microdroid_init_debug_policy",
"microdroid_init_rc",
"microdroid_ueventd_rc",
"microdroid_launcher",
@@ -69,7 +70,6 @@
"libartpalette-system",
"apexd.microdroid",
- "atrace",
"debuggerd",
"linker",
"tombstoned.microdroid",
@@ -85,13 +85,15 @@
"microdroid_property_contexts",
"mke2fs.microdroid",
- // TODO(b/195425111) these should be added automatically
- "libcrypto", // used by many (init_second_stage, microdroid_manager, toybox, etc)
- "liblzma", // used by init_second_stage
-
"libvm_payload", // used by payload to interact with microdroid manager
"prng_seeder_microdroid",
+
+ // Binaries required to capture traces in Microdroid.
+ "atrace",
+ "traced",
+ "traced_probes",
+ "perfetto",
] + microdroid_shell_and_utilities,
multilib: {
common: {
diff --git a/microdroid/README.md b/microdroid/README.md
index 41278a5..28785fd 100644
--- a/microdroid/README.md
+++ b/microdroid/README.md
@@ -138,6 +138,7 @@
TEST_ROOT=/data/local/tmp/virt
adb shell /apex/com.android.virt/bin/vm run-app \
--log $TEST_ROOT/log.txt \
+--console $TEST_ROOT/console.txt \
PATH_TO_YOUR_APP \
$TEST_ROOT/MyApp.apk.idsig \
$TEST_ROOT/instance.img \
@@ -145,9 +146,9 @@
```
The last command lets you know the CID assigned to the VM. The console output
-from the VM is stored to `$TEST_ROOT/log.txt` file for debugging purpose. If you
-omit the `--log $TEST_ROOT/log.txt` option, it will be emitted to the current
-console.
+from the VM is stored to `$TEST_ROOT/console.txt` and logcat is stored to
+`$TEST_ROOT/log.txt` file for debugging purpose. If you omit `--log` or
+`--console` option, they will be emitted to the current console.
Stopping the VM can be done as follows:
@@ -159,12 +160,50 @@
invoked with the `--daemonize` flag. If the flag was not used, press Ctrl+C on
the console where the `vm run-app` command was invoked.
-## ADB
+## Debuggable microdroid
-On userdebug builds, you can have an adb connection to microdroid. To do so,
-first, delete `$TEST_ROOT/instance.img`; this is because changing debug settings
-requires a new instance. Then add the `--debug=full` flag to the
-`/apex/com.android.virt/bin/vm run-app` command, and then
+### Debugging features
+Microdroid supports following debugging features:
+
+- VM log
+- console output
+- kernel output
+- logcat output
+- [ramdump](../docs/debug/ramdump.md)
+- crashdump
+- [adb](#adb)
+- [gdb](#debugging-the-payload-on-microdroid)
+
+### Enabling debugging features
+There's two ways to enable the debugging features:
+
+#### Option 1) Running microdroid on AVF debug policy configured device
+
+microdroid can be started with debugging features by debug policies from the
+host. Host bootloader may provide debug policies to host OS's device tree for
+VMs.
+
+For protected VM, such device tree will be available in microdroid. microdroid
+can check which debuging features is enabled.
+
+Here are list of device tree properties for debugging features.
+
+- `/avf/guest/common/log`: `<1>` to enable kernel log and logcat. Ignored
+ otherwise.
+- `/avf/guest/common/ramdump`: `<1>` to enable ramdump. Ignored otherwise.
+- `/avf/guest/microdroid/adb`: `<1>` to enable `adb`. Ignored otherwise.
+
+#### Option 2) Lauching microdroid with debug level.
+
+microdroid can be started with debugging features. To do so, first, delete
+`$TEST_ROOT/instance.img`; this is because changing debug settings requires a
+new instance. Then add the `--debug=full` flag to the
+`/apex/com.android.virt/bin/vm run-app` command. This will enable all debugging
+features.
+
+### ADB
+
+If `adb` connection is enabled, launch following command.
```sh
vm_shell
@@ -175,7 +214,7 @@
Once you have an adb connection with `vm_shell`, `localhost:8000` will be the
serial of microdroid.
-## Debugging the payload on microdroid
+### Debugging the payload on microdroid
Like a normal adb device, you can debug native processes using `lldbclient.py`
script, either by running a new process, or attaching to an existing process.
diff --git a/microdroid/init.rc b/microdroid/init.rc
index ce0cab4..c997bfd 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -21,13 +21,9 @@
write /linkerconfig/ld.config.txt \#
chmod 644 /linkerconfig/ld.config.txt
-# If VM is debuggable, send logs to outside ot the VM via the serial console.
-# If non-debuggable, logs are internally consumed at /dev/null
-on early-init && property:ro.boot.microdroid.debuggable=1
- setprop ro.log.file_logger.path /dev/hvc2
-
-on early-init && property:ro.boot.microdroid.debuggable=0
- setprop ro.log.file_logger.path /dev/null
+ # Applies debug policy to decide whether to enable adb, adb root, and logcat.
+ # We don't directly exec the binary to specify stdio_to_kmsg.
+ exec_start init_debug_policy
on init
mkdir /mnt/apk 0755 system system
@@ -39,7 +35,10 @@
restorecon /mnt/extra-apk
# Wait for apexd to finish activating APEXes before starting more processes.
- wait_for_prop apexd.status activated
+ # Microdroid starts apexd in VM mode in which apexd doesn't wait for init after setting
+ # apexd.status to activated, but immediately transitions to ready. Therefore, it's not safe to
+ # wait for the activated status, by the time this line is reached it may be already be ready.
+ wait_for_prop apexd.status ready
perform_apex_config
# Notify to microdroid_manager that perform_apex_config is done.
@@ -47,8 +46,6 @@
# payloads are not designed to run with bootstrap bionic
setprop apex_config.done true
- setprop ro.debuggable ${ro.boot.microdroid.debuggable:-0}
-
on property:microdroid_manager.init_done=1
# Stop ueventd to save memory
stop ueventd
@@ -57,7 +54,7 @@
# Mount tracefs (with GID=AID_READTRACEFS)
mount tracefs tracefs /sys/kernel/tracing gid=3012
-on init && property:ro.boot.adb.enabled=1
+on property:init_debug_policy.adbd.enabled=1
start adbd
# Mount filesystems and start core system services.
@@ -98,7 +95,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
@@ -179,3 +176,8 @@
group shell log readproc
seclabel u:r:shell:s0
setenv HOSTNAME console
+
+service init_debug_policy /system/bin/init_debug_policy
+ oneshot
+ disabled
+ stdio_to_kmsg
diff --git a/microdroid/init_debug_policy/Android.bp b/microdroid/init_debug_policy/Android.bp
new file mode 100644
index 0000000..afc2e73
--- /dev/null
+++ b/microdroid/init_debug_policy/Android.bp
@@ -0,0 +1,15 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_binary {
+ name: "microdroid_init_debug_policy",
+ srcs: ["src/init_debug_policy.rs"],
+ stem: "init_debug_policy",
+ rustlibs: [
+ "librustutils",
+ ],
+ installable: false, // match with microdroid_init_rc.
+ bootstrap: true,
+ prefer_rlib: true,
+}
diff --git a/microdroid/init_debug_policy/src/init_debug_policy.rs b/microdroid/init_debug_policy/src/init_debug_policy.rs
new file mode 100644
index 0000000..6c80926
--- /dev/null
+++ b/microdroid/init_debug_policy/src/init_debug_policy.rs
@@ -0,0 +1,57 @@
+// 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.
+
+//! Applies debug policies when booting microdroid
+
+use rustutils::system_properties;
+use rustutils::system_properties::PropertyWatcherError;
+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)
+}
+
+fn main() -> Result<(), PropertyWatcherError> {
+ // If VM is debuggable or debug policy says so, send logs to outside ot the VM via the serial console.
+ // Otherwise logs are internally consumed at /dev/null
+ let log_path = if system_properties::read_bool("ro.boot.microdroid.debuggable", false)?
+ || get_debug_policy_bool("/sys/firmware/devicetree/base/avf/guest/common/log")
+ .unwrap_or_default()
+ {
+ "/dev/hvc2"
+ } else {
+ "/dev/null"
+ };
+ system_properties::write("ro.log.file_logger.path", log_path)?;
+
+ let (adbd_enabled, debuggable) = if system_properties::read_bool("ro.boot.adb.enabled", false)?
+ || get_debug_policy_bool("/sys/firmware/devicetree/base/avf/guest/microdroid/adb")
+ .unwrap_or_default()
+ {
+ // debuggable is required for adb root and bypassing adb authorization.
+ ("1", "1")
+ } else {
+ ("0", "0")
+ };
+ system_properties::write("init_debug_policy.adbd.enabled", adbd_enabled)?;
+ system_properties::write("ro.debuggable", debuggable)?;
+
+ Ok(())
+}
diff --git a/microdroid/initrd/src/main.rs b/microdroid/initrd/src/main.rs
index c5515af..8f06f09 100644
--- a/microdroid/initrd/src/main.rs
+++ b/microdroid/initrd/src/main.rs
@@ -90,7 +90,7 @@
let initrd_size: usize = initrd_bc_size - bc_size - INITRD_FOOTER_LEN;
- initrd_bc.seek(SeekFrom::Start(0))?;
+ initrd_bc.rewind()?;
copyfile2file(&mut initrd_bc, &mut initrd, initrd_size)?;
copyfile2file(&mut initrd_bc, &mut bootconfig, bc_size)?;
Ok(())
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/kdump/kexec.c b/microdroid/kdump/kexec.c
index 8d88951..d3e8e02 100644
--- a/microdroid/kdump/kexec.c
+++ b/microdroid/kdump/kexec.c
@@ -23,6 +23,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
@@ -53,6 +54,20 @@
if (syscall(SYS_kexec_file_load, open_checked(KERNEL), open_checked(INITRD), cmdline_len,
CMDLINE, KEXEC_FILE_ON_CRASH) == -1) {
fprintf(stderr, "Failed to load panic kernel: %s\n", strerror(errno));
+ if (errno == EADDRNOTAVAIL) {
+ struct stat st;
+ off_t kernel_size = 0;
+ off_t initrd_size = 0;
+
+ if (stat(KERNEL, &st) == 0) {
+ kernel_size = st.st_size;
+ }
+ if (stat(INITRD, &st) == 0) {
+ initrd_size = st.st_size;
+ }
+ fprintf(stderr, "Image size too big? %s:%ld bytes, %s:%ld bytes", KERNEL, kernel_size,
+ INITRD, initrd_size);
+ }
return 1;
}
return 0;
diff --git a/microdroid/payload/Android.bp b/microdroid/payload/Android.bp
index f77c037..4814a64 100644
--- a/microdroid/payload/Android.bp
+++ b/microdroid/payload/Android.bp
@@ -36,26 +36,13 @@
],
}
-cc_binary_host {
- name: "mk_payload",
+java_library_host {
+ name: "microdroid_payload_metadata",
srcs: [
- "mk_payload.cc",
+ "src/**/*.java",
+ "metadata.proto",
],
- static_libs: [
- "lib_microdroid_metadata_proto",
- "libbase",
- "libcdisk_spec",
- "libcuttlefish_fs",
- "libcuttlefish_utils",
- "libext2_uuid",
- "libimage_aggregator",
- "libjsoncpp",
- "liblog",
- "libprotobuf-cpp-full",
- "libprotobuf-cpp-lite",
- "libsparse",
- "libxml2",
- "libz",
- ],
- static_executable: true,
+ proto: {
+ type: "lite",
+ },
}
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/payload/metadata.proto b/microdroid/payload/metadata.proto
index c74c23b..6b999af 100644
--- a/microdroid/payload/metadata.proto
+++ b/microdroid/payload/metadata.proto
@@ -18,6 +18,9 @@
package android.microdroid;
+option java_package = "com.android.virt";
+option java_outer_classname = "PayloadMetadataProtos";
+
// Metadata is the body of the "metadata" partition
message Metadata {
uint32 version = 1;
diff --git a/microdroid/payload/src/com/android/virt/PayloadMetadata.java b/microdroid/payload/src/com/android/virt/PayloadMetadata.java
new file mode 100644
index 0000000..c2f0a7f
--- /dev/null
+++ b/microdroid/payload/src/com/android/virt/PayloadMetadata.java
@@ -0,0 +1,49 @@
+package com.android.virt;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/** Provides utility to create/read/write PayloadMetadata */
+public class PayloadMetadata {
+ public static void write(PayloadMetadataProtos.Metadata metadata, File file)
+ throws IOException {
+ byte[] message = metadata.toByteArray();
+
+ try (DataOutputStream os = new DataOutputStream(new FileOutputStream(file))) {
+ // write length prefix (4-byte, big-endian)
+ os.writeInt(message.length);
+ // write the message
+ os.write(message);
+ }
+ }
+
+ public static PayloadMetadataProtos.Metadata metadata(
+ String configPath,
+ PayloadMetadataProtos.ApkPayload apk,
+ Iterable<? extends PayloadMetadataProtos.ApexPayload> apexes) {
+ return PayloadMetadataProtos.Metadata.newBuilder()
+ .setVersion(1)
+ .setConfigPath(configPath)
+ .setApk(apk)
+ .addAllApexes(apexes)
+ .build();
+ }
+
+ public static PayloadMetadataProtos.ApkPayload apk(String name) {
+ return PayloadMetadataProtos.ApkPayload.newBuilder()
+ .setName(name)
+ .setPayloadPartitionName("microdroid-apk")
+ .setIdsigPartitionName("microdroid-apk-idsig")
+ .build();
+ }
+
+ public static PayloadMetadataProtos.ApexPayload apex(String name) {
+ return PayloadMetadataProtos.ApexPayload.newBuilder()
+ .setName(name)
+ .setIsFactory(true)
+ .setPartitionName(name)
+ .build();
+ }
+}
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index 383f371..495d3bb 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -19,10 +19,9 @@
"libbinder_rs",
"libbyteorder",
"libcap_rust",
- "libdiced",
- "libdiced_open_dice_cbor",
+ "libciborium",
+ "libdiced_open_dice",
"libdiced_sample_inputs",
- "libdiced_utils",
"libglob",
"libhex",
"libitertools",
@@ -47,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 a7288b6..3a2a1e6 100644
--- a/microdroid_manager/src/dice.rs
+++ b/microdroid_manager/src/dice.rs
@@ -14,13 +14,16 @@
//! Logic for handling the DICE values and boot operations.
-use anyhow::{bail, Context, Error, Result};
+use anyhow::{anyhow, bail, Context, Error, Result};
use byteorder::{NativeEndian, ReadBytesExt};
-use diced_open_dice_cbor::{
- Config, ContextImpl, DiceMode, Hash, Hidden, InputValues, OpenDiceCborContext, CDI_SIZE,
+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;
@@ -29,22 +32,14 @@
use std::ptr::null_mut;
use std::slice;
-/// Artifacts that are kept in the process address space after the artifacts from the driver have
-/// been consumed.
-pub struct DiceContext {
- pub cdi_attest: [u8; CDI_SIZE],
- pub cdi_seal: [u8; CDI_SIZE],
- pub bcc: Vec<u8>,
-}
-
-impl DiceContext {
- pub fn get_sealing_key(&self, salt: &[u8], identifier: &[u8], keysize: u32) -> Result<ZVec> {
- // Deterministically derive a key to use for sealing data based on salt. Use different salt
- // for different keys.
- let mut key = ZVec::new(keysize as usize)?;
- hkdf(&mut key, Md::sha256(), &self.cdi_seal, salt, identifier)?;
- Ok(key)
- }
+/// Derives a sealing key from the DICE sealing CDI.
+pub fn derive_sealing_key(
+ dice_artifacts: &dyn DiceArtifacts,
+ salt: &[u8],
+ info: &[u8],
+ key: &mut [u8],
+) -> Result<()> {
+ Ok(hkdf(key, Md::sha256(), dice_artifacts.cdi_seal(), salt, info)?)
}
/// Artifacts that are mapped into the process address space from the driver.
@@ -53,14 +48,19 @@
driver_path: PathBuf,
mmap_addr: *mut c_void,
mmap_size: usize,
- cdi_attest: &'a [u8; CDI_SIZE],
- cdi_seal: &'a [u8; CDI_SIZE],
- bcc: &'a [u8],
+ bcc_handover: BccHandover<'a>,
},
- Fake(DiceContext),
+ Fake(OwnedDiceArtifacts),
}
impl DiceDriver<'_> {
+ fn dice_artifacts(&self) -> &dyn DiceArtifacts {
+ match self {
+ Self::Real { bcc_handover, .. } => bcc_handover,
+ Self::Fake(owned_dice_artifacts) => owned_dice_artifacts,
+ }
+ }
+
pub fn new(driver_path: &Path) -> Result<Self> {
if driver_path.exists() {
log::info!("Using DICE values from driver");
@@ -68,13 +68,9 @@
bail!("Strict boot requires DICE value from driver but none were found");
} else {
log::warn!("Using sample DICE values");
- let (cdi_attest, cdi_seal, bcc) = diced_sample_inputs::make_sample_bcc_and_cdis()
+ let dice_artifacts = diced_sample_inputs::make_sample_bcc_and_cdis()
.expect("Failed to create sample dice artifacts.");
- return Ok(Self::Fake(DiceContext {
- cdi_attest: cdi_attest[..].try_into().unwrap(),
- cdi_seal: cdi_seal[..].try_into().unwrap(),
- bcc,
- }));
+ return Ok(Self::Fake(dice_artifacts));
};
let mut file = fs::File::open(driver_path)
@@ -96,41 +92,24 @@
// accessible and not referenced from anywhere else.
let mmap_buf =
unsafe { slice::from_raw_parts((mmap_addr as *const u8).as_ref().unwrap(), mmap_size) };
- // Very inflexible parsing / validation of the BccHandover data. Assumes deterministically
- // encoded CBOR.
- //
- // BccHandover = {
- // 1 : bstr .size 32, ; CDI_Attest
- // 2 : bstr .size 32, ; CDI_Seal
- // 3 : Bcc, ; Certificate chain
- // }
- if mmap_buf[0..4] != [0xa3, 0x01, 0x58, 0x20]
- || mmap_buf[36..39] != [0x02, 0x58, 0x20]
- || mmap_buf[71] != 0x03
- {
- bail!("BccHandover format mismatch");
- }
+ let bcc_handover =
+ bcc_handover_parse(mmap_buf).map_err(|_| anyhow!("Failed to parse Bcc Handover"))?;
Ok(Self::Real {
driver_path: driver_path.to_path_buf(),
mmap_addr,
mmap_size,
- cdi_attest: mmap_buf[4..36].try_into().unwrap(),
- cdi_seal: mmap_buf[39..71].try_into().unwrap(),
- bcc: &mmap_buf[72..],
+ bcc_handover,
})
}
- pub fn get_sealing_key(&self, identifier: &[u8]) -> Result<ZVec> {
+ /// Derives a sealing key of `key_length` bytes from the DICE sealing CDI.
+ pub fn get_sealing_key(&self, identifier: &[u8], key_length: usize) -> Result<ZVec> {
// Deterministically derive a key to use for sealing data, rather than using the CDI
// directly, so we have the chance to rotate the key if needed. A salt isn't needed as the
// input key material is already cryptographically strong.
- let cdi_seal = match self {
- Self::Real { cdi_seal, .. } => cdi_seal,
- Self::Fake(fake) => &fake.cdi_seal,
- };
+ let mut key = ZVec::new(key_length)?;
let salt = &[];
- let mut key = ZVec::new(32)?;
- hkdf(&mut key, Md::sha256(), cdi_seal, salt, identifier)?;
+ derive_sealing_key(self.dice_artifacts(), salt, identifier, &mut key)?;
Ok(key)
}
@@ -141,7 +120,7 @@
authority_hash: Hash,
debug: bool,
hidden: Hidden,
- ) -> Result<DiceContext> {
+ ) -> Result<OwnedDiceArtifacts> {
let input_values = InputValues::new(
code_hash,
Config::Descriptor(config_desc),
@@ -149,24 +128,21 @@
if debug { DiceMode::kDiceModeDebug } else { DiceMode::kDiceModeNormal },
hidden,
);
- let (cdi_attest, cdi_seal, bcc) = match &self {
- Self::Real { cdi_attest, cdi_seal, bcc, .. } => (*cdi_attest, *cdi_seal, *bcc),
- Self::Fake(fake) => (&fake.cdi_attest, &fake.cdi_seal, fake.bcc.as_slice()),
- };
- let (cdi_attest, cdi_seal, bcc) = OpenDiceCborContext::new()
- .bcc_main_flow(cdi_attest, cdi_seal, bcc, &input_values)
- .context("DICE derive from driver")?;
+ let current_dice_artifacts = self.dice_artifacts();
+ let next_dice_artifacts = retry_bcc_main_flow(
+ current_dice_artifacts.cdi_attest(),
+ current_dice_artifacts.cdi_seal(),
+ current_dice_artifacts.bcc().ok_or_else(|| anyhow!("bcc is none"))?,
+ &input_values,
+ )
+ .context("DICE derive from driver")?;
if let Self::Real { driver_path, .. } = &self {
// Writing to the device wipes the artifacts. The string is ignored by the driver but
// included for documentation.
fs::write(driver_path, "wipe")
.map_err(|err| Error::new(err).context("Wiping driver"))?;
}
- Ok(DiceContext {
- cdi_attest: cdi_attest[..].try_into().unwrap(),
- cdi_seal: cdi_seal[..].try_into().unwrap(),
- bcc,
- })
+ Ok(next_dice_artifacts)
}
}
@@ -183,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/instance.rs b/microdroid_manager/src/instance.rs
index 96e9360..6900ea5 100644
--- a/microdroid_manager/src/instance.rs
+++ b/microdroid_manager/src/instance.rs
@@ -142,9 +142,9 @@
self.file.read_exact(&mut header)?;
// Decrypt and authenticate the data (along with the header).
- let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER)?;
- let plaintext =
- decrypt_aead(Cipher::aes_256_gcm(), &key, Some(&nonce), &header, &data, &tag)?;
+ let cipher = Cipher::aes_256_gcm();
+ let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER, cipher.key_len())?;
+ let plaintext = decrypt_aead(cipher, &key, Some(&nonce), &header, &data, &tag)?;
let microdroid_data = serde_cbor::from_slice(plaintext.as_slice())?;
Ok(Some(microdroid_data))
@@ -188,10 +188,10 @@
self.file.write_all(nonce.as_ref())?;
// Then encrypt and sign the data.
- let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER)?;
+ let cipher = Cipher::aes_256_gcm();
+ let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER, cipher.key_len())?;
let mut tag = [0; AES_256_GCM_TAG_LENGTH];
- let ciphertext =
- encrypt_aead(Cipher::aes_256_gcm(), &key, Some(&nonce), &header, &data, &mut tag)?;
+ let ciphertext = encrypt_aead(cipher, &key, Some(&nonce), &header, &data, &mut tag)?;
// Persist the encrypted payload data and the tag.
self.file.write_all(&ciphertext)?;
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 777a42c..fa96bf4 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::{DiceContext, DiceDriver};
+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;
@@ -34,11 +34,12 @@
use anyhow::{anyhow, bail, ensure, Context, Error, Result};
use apkverify::{get_public_key_der, verify, V4Signature};
use binder::Strong;
-use diced_utils::cbor::{encode_header, encode_number};
+use diced_open_dice::OwnedDiceArtifacts;
use glob::glob;
use itertools::sorted;
use libc::VMADDR_CID_HOST;
use log::{error, info, warn};
+use keystore2_crypto::ZVec;
use microdroid_metadata::{write_metadata, Metadata, PayloadMetadata};
use microdroid_payload_config::{OsConfig, Task, TaskType, VmPayloadConfig};
use nix::fcntl::{fcntl, F_SETFD, FdFlag};
@@ -88,7 +89,7 @@
const ENCRYPTEDSTORE_BACKING_DEVICE: &str = "/dev/block/by-name/encryptedstore";
const ENCRYPTEDSTORE_KEY_IDENTIFIER: &str = "encryptedstore_key";
-const ENCRYPTEDSTORE_KEYSIZE: u32 = 32;
+const ENCRYPTEDSTORE_KEYSIZE: usize = 32;
#[derive(thiserror::Error, Debug)]
enum MicrodroidError {
@@ -268,7 +269,7 @@
dice: DiceDriver,
verified_data: &MicrodroidData,
payload_metadata: &PayloadMetadata,
-) -> Result<DiceContext> {
+) -> Result<OwnedDiceArtifacts> {
// Calculate compound digests of code and authorities
let mut code_hash_ctx = Sha512::new();
let mut authority_hash_ctx = Sha512::new();
@@ -285,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 {
@@ -413,12 +374,12 @@
// To minimize the exposure to untrusted data, derive dice profile as soon as possible.
info!("DICE derivation for payload");
- let dice_context = dice_derivation(dice, &verified_data, &payload_metadata)?;
+ let dice_artifacts = dice_derivation(dice, &verified_data, &payload_metadata)?;
// Run encryptedstore binary to prepare the storage
let encryptedstore_child = if Path::new(ENCRYPTEDSTORE_BACKING_DEVICE).exists() {
info!("Preparing encryptedstore ...");
- Some(prepare_encryptedstore(&dice_context).context("encryptedstore run")?)
+ Some(prepare_encryptedstore(&dice_artifacts).context("encryptedstore run")?)
} else {
None
};
@@ -473,7 +434,7 @@
// Wait until zipfuse has mounted the APKs so we can access the payload
zipfuse.wait_until_done()?;
- register_vm_payload_service(allow_restricted_apis, service.clone(), dice_context)?;
+ register_vm_payload_service(allow_restricted_apis, service.clone(), dice_artifacts)?;
// Wait for encryptedstore to finish mounting the storage (if enabled) before setting
// microdroid_manager.init_done. Reason is init stops uneventd after that.
@@ -719,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())?;
@@ -790,16 +754,23 @@
}
fn get_public_key_from_apk(apk: &str, root_hash_trustful: bool) -> Result<Box<[u8]>> {
+ let current_sdk = get_current_sdk()?;
if !root_hash_trustful {
- verify(apk).context(MicrodroidError::PayloadVerificationFailed(format!(
+ verify(apk, current_sdk).context(MicrodroidError::PayloadVerificationFailed(format!(
"failed to verify {}",
apk
)))
} else {
- get_public_key_der(apk)
+ get_public_key_der(apk, current_sdk)
}
}
+fn get_current_sdk() -> Result<u32> {
+ let current_sdk = system_properties::read("ro.build.version.sdk")?;
+ let current_sdk = current_sdk.ok_or_else(|| anyhow!("SDK version missing"))?;
+ current_sdk.parse().context("Malformed SDK version")
+}
+
fn load_config(payload_metadata: PayloadMetadata) -> Result<VmPayloadConfig> {
match payload_metadata {
PayloadMetadata::config_path(path) => {
@@ -907,7 +878,7 @@
buf.iter().map(|b| format!("{:02X}", b)).collect()
}
-fn prepare_encryptedstore(dice: &DiceContext) -> Result<Child> {
+fn prepare_encryptedstore(dice_artifacts: &OwnedDiceArtifacts) -> Result<Child> {
// Use a fixed salt to scope the derivation to this API.
// Generated using hexdump -vn32 -e'14/1 "0x%02X, " 1 "\n"' /dev/urandom
// TODO(b/241541860) : Move this (& other salts) to a salt container, i.e. a global enum
@@ -916,11 +887,8 @@
0x6F, 0xB3, 0xF9, 0x40, 0xCE, 0xDD, 0x99, 0x40, 0xAA, 0xA7, 0x0E, 0x92, 0x73, 0x90, 0x86,
0x4A, 0x75,
];
- let key = dice.get_sealing_key(
- &salt,
- ENCRYPTEDSTORE_KEY_IDENTIFIER.as_bytes(),
- ENCRYPTEDSTORE_KEYSIZE,
- )?;
+ let mut key = ZVec::new(ENCRYPTEDSTORE_KEYSIZE)?;
+ derive_sealing_key(dice_artifacts, &salt, ENCRYPTEDSTORE_KEY_IDENTIFIER.as_bytes(), &mut key)?;
let mut cmd = Command::new(ENCRYPTEDSTORE_BIN);
cmd.arg("--blkdevice")
diff --git a/microdroid_manager/src/vm_payload_service.rs b/microdroid_manager/src/vm_payload_service.rs
index 98b9f2b..96f51f0 100644
--- a/microdroid_manager/src/vm_payload_service.rs
+++ b/microdroid_manager/src/vm_payload_service.rs
@@ -14,22 +14,21 @@
//! Implementation of the AIDL interface `IVmPayloadService`.
-use crate::dice::DiceContext;
+use crate::dice::derive_sealing_key;
use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
BnVmPayloadService, IVmPayloadService, VM_PAYLOAD_SERVICE_SOCKET_NAME};
use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
use anyhow::Result;
use binder::{Interface, BinderFeatures, ExceptionCode, Status, Strong};
+use diced_open_dice::{DiceArtifacts, OwnedDiceArtifacts};
use log::{error, info};
-use openssl::hkdf::hkdf;
-use openssl::md::Md;
use rpcbinder::RpcServer;
/// Implementation of `IVmPayloadService`.
struct VmPayloadService {
allow_restricted_apis: bool,
virtual_machine_service: Strong<dyn IVirtualMachineService>,
- dice: DiceContext,
+ dice: OwnedDiceArtifacts,
}
impl IVmPayloadService for VmPayloadService {
@@ -48,7 +47,7 @@
0xB7, 0xA8, 0x43, 0x92,
];
let mut secret = vec![0; size.try_into().unwrap()];
- hkdf(&mut secret, Md::sha256(), &self.dice.cdi_seal, &salt, identifier).map_err(|e| {
+ derive_sealing_key(&self.dice, &salt, identifier, &mut secret).map_err(|e| {
error!("Failed to derive VM instance secret: {:?}", e);
Status::new_service_specific_error(-1, None)
})?;
@@ -57,12 +56,16 @@
fn getDiceAttestationChain(&self) -> binder::Result<Vec<u8>> {
self.check_restricted_apis_allowed()?;
- Ok(self.dice.bcc.clone())
+ if let Some(bcc) = self.dice.bcc() {
+ Ok(bcc.to_vec())
+ } else {
+ Err(Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some("bcc is none")))
+ }
}
fn getDiceAttestationCdi(&self) -> binder::Result<Vec<u8>> {
self.check_restricted_apis_allowed()?;
- Ok(self.dice.cdi_attest.to_vec())
+ Ok(self.dice.cdi_attest().to_vec())
}
}
@@ -73,7 +76,7 @@
fn new(
allow_restricted_apis: bool,
vm_service: Strong<dyn IVirtualMachineService>,
- dice: DiceContext,
+ dice: OwnedDiceArtifacts,
) -> Self {
Self { allow_restricted_apis, virtual_machine_service: vm_service, dice }
}
@@ -92,7 +95,7 @@
pub(crate) fn register_vm_payload_service(
allow_restricted_apis: bool,
vm_service: Strong<dyn IVirtualMachineService>,
- dice: DiceContext,
+ dice: OwnedDiceArtifacts,
) -> Result<()> {
let vm_payload_binder = BnVmPayloadService::new_binder(
VmPayloadService::new(allow_restricted_apis, vm_service, dice),
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 0d6a9a4..0d845f9 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -13,8 +13,8 @@
],
rustlibs: [
"libaarch64_paging",
+ "libbssl_ffi_nostd",
"libbuddy_system_allocator",
- "libdice_nostd", // TODO(b/267575445): Remove this library once the migration is done.
"libdiced_open_dice_nostd",
"libfdtpci",
"liblibfdt",
@@ -22,7 +22,10 @@
"libonce_cell_nostd",
"libpvmfw_avb_nostd",
"libpvmfw_embedded_key",
+ "libpvmfw_fdt_template",
+ "libstatic_assertions",
"libtinyvec_nostd",
+ "libuuid_nostd",
"libvirtio_drivers",
"libvmbase",
"libzeroize_nostd",
@@ -51,6 +54,7 @@
// partition image. This is just to package the unstripped file into the
// symbols zip file for debugging purpose.
installable: true,
+ native_coverage: false,
}
raw_binary {
@@ -65,6 +69,22 @@
},
}
+// Provide pvmfw.bin binary regardless of the architecture for building test.
+// Note that skipping tests on unsupported device is easy
+// while configuring server configuration to make such tests to run on working
+// devices.
+prebuilt_etc {
+ name: "pvmfw_test",
+ filename: "pvmfw_test.bin",
+ target: {
+ android_arm64: {
+ src: ":pvmfw_bin",
+ },
+ },
+ src: "empty_file",
+ installable: false,
+}
+
prebuilt_etc {
name: "pvmfw_embedded_key",
src: ":avb_testkey_rsa4096_pub_bin",
@@ -99,6 +119,37 @@
installable: false,
}
+// platform.dts is passed to clang for macro preprocessing, and then compiled to dtbo using dtc.
+// The raw content of the dtbo file is then written as a Rust byte array.
+genrule {
+ name: "pvmfw_fdt_template_rs",
+ srcs: [
+ "platform.dts",
+ ":arm_dt_bindings_headers", // implicit dependency
+ ],
+ out: ["lib.rs"],
+ tools: ["dtc"],
+ cmd: "prebuilts/clang/host/linux-x86/clang-r487747/bin/clang " + // UGLY!!!
+ "-E -P -x assembler-with-cpp -I external/arm-trusted-firmware/include " +
+ "-o $(genDir)/preprocessed.dts $(location platform.dts) && " +
+ "$(location dtc) -I dts -O dtb -o $(genDir)/compiled.dtbo $(genDir)/preprocessed.dts && " +
+ "(" +
+ " echo '#![no_std]';" +
+ " echo '#![allow(missing_docs)]';" +
+ " echo 'pub const RAW: &[u8] = &[';" +
+ " xxd -i < $(genDir)/compiled.dtbo;" +
+ " echo '];';" +
+ ") > $(out)",
+}
+
+rust_library_rlib {
+ name: "libpvmfw_fdt_template",
+ defaults: ["vmbase_ffi_defaults"],
+ prefer_rlib: true,
+ srcs: [":pvmfw_fdt_template_rs"],
+ crate_name: "pvmfw_fdt_template",
+}
+
bootimg {
name: "pvmfw_img",
stem: "pvmfw.img",
diff --git a/pvmfw/README.md b/pvmfw/README.md
index 1e4b605..04ad8c4 100644
--- a/pvmfw/README.md
+++ b/pvmfw/README.md
@@ -61,13 +61,27 @@
Starting in Android T, the `PRODUCT_BUILD_PVMFW_IMAGE` build variable controls
the generation of `pvmfw.img`, a new [ABL partition][ABL-part] containing the
-pvmfw binary and following the internal format of the [`boot`][boot-img]
-partition, intended to be verified and loaded by ABL on AVF-compatible devices.
+pvmfw binary (sometimes called "`pvmfw.bin`") and following the internal format
+of the [`boot`][boot-img] partition, intended to be verified and loaded by ABL
+on AVF-compatible devices.
+
+Once ABL has verified the `pvmfw.img` chained static partition, the contained
+[`boot.img` header][boot-img] may be used to obtain the size of the `pvmfw.bin`
+image (recorded in the `kernel_size` field), as it already does for the kernel
+itself. In accordance with the header format, the `kernel_size` bytes of the
+partition following the header will be the `pvmfw.bin` image.
+
+Note that when it gets executed in the context of a pVM, `pvmfw` expects to have
+been loaded at 4KiB-aligned intermediate physical address (IPA) so if ABL loads
+the `pvmfw.bin` image without respecting this alignment, it is the
+responsibility of the hypervisor to either reject the image or copy it into
+guest address space with the right alignment.
To support pKVM, ABL is expected to describe the region using a reserved memory
device tree node where both address and size have been properly aligned to the
-page size used by the hypervisor. For example, the following node describes a
-region of size `0x40000` at address `0x80000000`:
+page size used by the hypervisor. This single region must include both the pvmfw
+binary image and its configuration data (see below). For example, the following
+node describes a region of size `0x40000` at address `0x80000000`:
```
reserved-memory {
...
@@ -220,3 +234,31 @@
[dice-dt]: https://www.kernel.org/doc/Documentation/devicetree/bindings/reserved-memory/google%2Copen-dice.yaml
[Layering]: https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/specification.md#layering-details
[Trusty-BCC]: https://android.googlesource.com/trusty/lib/+/1696be0a8f3a7103/lib/hwbcc/common/swbcc.c#554
+
+#### pVM Device Tree Overlay
+
+Config header can provide a DTBO to be overlaid on top of the baseline device
+tree from crosvm.
+
+The DTBO may contain debug policies as follows.
+
+```
+/ {
+ fragment@avf {
+ target-path = "/";
+
+ __overlay__ {
+ avf {
+ /* your debug policy here */
+ };
+ };
+ };
+}; /* end of avf */
+```
+
+For specifying DTBO, host bootloader should apply the DTBO to both host
+OS's device tree and config header of `pvmfw`. Both `virtualizationmanager` and
+`pvmfw` will prepare for debugging features.
+
+For details about device tree properties for debug policies, see
+[microdroid's debugging policy guide](../microdroid/README.md#option-1-running-microdroid-on-avf-debug-policy-configured-device).
diff --git a/pvmfw/avb/Android.bp b/pvmfw/avb/Android.bp
index 0c1e392..7ed4895 100644
--- a/pvmfw/avb/Android.bp
+++ b/pvmfw/avb/Android.bp
@@ -8,11 +8,11 @@
srcs: ["src/lib.rs"],
prefer_rlib: true,
rustlibs: [
- "libavb_bindgen",
+ "libavb_bindgen_nostd",
"libtinyvec_nostd",
],
whole_static_libs: [
- "libavb_non_debug",
+ "libavb_baremetal",
],
}
diff --git a/pvmfw/avb/src/lib.rs b/pvmfw/avb/src/lib.rs
index 8fea162..d83737f 100644
--- a/pvmfw/avb/src/lib.rs
+++ b/pvmfw/avb/src/lib.rs
@@ -15,8 +15,6 @@
//! A library implementing the payload verification for pvmfw with libavb
#![cfg_attr(not(test), no_std)]
-// For usize.checked_add_signed(isize), available in Rust 1.66.0
-#![feature(mixed_integer_ops)]
mod descriptor;
mod error;
diff --git a/pvmfw/avb/src/verify.rs b/pvmfw/avb/src/verify.rs
index 1a79c83..b03506c 100644
--- a/pvmfw/avb/src/verify.rs
+++ b/pvmfw/avb/src/verify.rs
@@ -22,14 +22,16 @@
use core::ffi::c_char;
/// Verified data returned when the payload verification succeeds.
-#[derive(Debug)]
-pub struct VerifiedBootData {
+#[derive(Debug, PartialEq, Eq)]
+pub struct VerifiedBootData<'a> {
/// DebugLevel of the VM.
pub debug_level: DebugLevel,
/// Kernel digest.
pub kernel_digest: Digest,
/// Initrd digest if initrd exists.
pub initrd_digest: Option<Digest>,
+ /// Trusted public key.
+ pub public_key: &'a [u8],
}
/// This enum corresponds to the `DebugLevel` in `VirtualMachineConfig`.
@@ -94,11 +96,11 @@
}
/// Verifies the payload (signed kernel + initrd) against the trusted public key.
-pub fn verify_payload(
+pub fn verify_payload<'a>(
kernel: &[u8],
initrd: Option<&[u8]>,
- trusted_public_key: &[u8],
-) -> Result<VerifiedBootData, AvbSlotVerifyError> {
+ trusted_public_key: &'a [u8],
+) -> Result<VerifiedBootData<'a>, AvbSlotVerifyError> {
let mut payload = Payload::new(kernel, initrd, trusted_public_key);
let mut ops = Ops::from(&mut payload);
let kernel_verify_result = ops.verify_partition(PartitionName::Kernel.as_cstr())?;
@@ -119,6 +121,7 @@
debug_level: DebugLevel::None,
kernel_digest: kernel_descriptor.digest,
initrd_digest: None,
+ public_key: trusted_public_key,
});
}
@@ -142,5 +145,6 @@
debug_level,
kernel_digest: kernel_descriptor.digest,
initrd_digest: Some(initrd_descriptor.digest),
+ public_key: trusted_public_key,
})
}
diff --git a/pvmfw/avb/tests/api_test.rs b/pvmfw/avb/tests/api_test.rs
index 261d8a8..78f274a 100644
--- a/pvmfw/avb/tests/api_test.rs
+++ b/pvmfw/avb/tests/api_test.rs
@@ -18,7 +18,7 @@
use anyhow::{anyhow, Result};
use avb_bindgen::{AvbFooter, AvbVBMetaImageHeader};
-use pvmfw_avb::{verify_payload, AvbSlotVerifyError, DebugLevel};
+use pvmfw_avb::{verify_payload, AvbSlotVerifyError, DebugLevel, VerifiedBootData};
use std::{fs, mem::size_of, ptr};
use utils::*;
@@ -53,17 +53,23 @@
#[test]
fn payload_expecting_no_initrd_passes_verification_with_no_initrd() -> Result<()> {
+ let public_key = load_trusted_public_key()?;
let verified_boot_data = verify_payload(
&fs::read(TEST_IMG_WITH_ONE_HASHDESC_PATH)?,
/*initrd=*/ None,
- &load_trusted_public_key()?,
+ &public_key,
)
.map_err(|e| anyhow!("Verification failed. Error: {}", e))?;
- assert_eq!(DebugLevel::None, verified_boot_data.debug_level);
- let digest = hash(&[&hex::decode("1111")?, &fs::read(UNSIGNED_TEST_IMG_PATH)?]);
- assert_eq!(digest, verified_boot_data.kernel_digest);
- assert!(verified_boot_data.initrd_digest.is_none());
+ let kernel_digest = hash(&[&hex::decode("1111")?, &fs::read(UNSIGNED_TEST_IMG_PATH)?]);
+ let expected_boot_data = VerifiedBootData {
+ debug_level: DebugLevel::None,
+ kernel_digest,
+ initrd_digest: None,
+ public_key: &public_key,
+ };
+ assert_eq!(expected_boot_data, verified_boot_data);
+
Ok(())
}
diff --git a/pvmfw/avb/tests/utils.rs b/pvmfw/avb/tests/utils.rs
index 8756d06..6713846 100644
--- a/pvmfw/avb/tests/utils.rs
+++ b/pvmfw/avb/tests/utils.rs
@@ -22,7 +22,7 @@
AvbVBMetaImageHeader,
};
use openssl::sha;
-use pvmfw_avb::{verify_payload, AvbSlotVerifyError, DebugLevel, Digest};
+use pvmfw_avb::{verify_payload, AvbSlotVerifyError, DebugLevel, Digest, VerifiedBootData};
use std::{
fs,
mem::{size_of, transmute, MaybeUninit},
@@ -102,23 +102,23 @@
initrd_salt: &[u8],
expected_debug_level: DebugLevel,
) -> Result<()> {
+ let public_key = load_trusted_public_key()?;
let kernel = load_latest_signed_kernel()?;
- let verified_boot_data = verify_payload(&kernel, Some(initrd), &load_trusted_public_key()?)
+ let verified_boot_data = verify_payload(&kernel, Some(initrd), &public_key)
.map_err(|e| anyhow!("Verification failed. Error: {}", e))?;
- assert_eq!(expected_debug_level, verified_boot_data.debug_level);
-
let footer = extract_avb_footer(&kernel)?;
- assert_eq!(
- hash(&[&hash(&[b"bootloader"]), &kernel[..usize::try_from(footer.original_image_size)?]]),
- verified_boot_data.kernel_digest,
- "Kernel digest is not equal to the expected."
- );
- assert_eq!(
- hash(&[&hash(&[initrd_salt]), initrd,]),
- verified_boot_data.initrd_digest.unwrap(),
- "initrd digest is not equal to the expected."
- );
+ let kernel_digest =
+ hash(&[&hash(&[b"bootloader"]), &kernel[..usize::try_from(footer.original_image_size)?]]);
+ let initrd_digest = Some(hash(&[&hash(&[initrd_salt]), initrd]));
+ let expected_boot_data = VerifiedBootData {
+ debug_level: expected_debug_level,
+ kernel_digest,
+ initrd_digest,
+ public_key: &public_key,
+ };
+ assert_eq!(expected_boot_data, verified_boot_data);
+
Ok(())
}
diff --git a/pvmfw/empty_file b/pvmfw/empty_file
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pvmfw/empty_file
diff --git a/pvmfw/platform.dts b/pvmfw/platform.dts
new file mode 100644
index 0000000..127f69a
--- /dev/null
+++ b/pvmfw/platform.dts
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2022 Google LLC
+ */
+
+#include <dt-bindings/interrupt-controller/arm-gic.h>
+
+#define PLACEHOLDER 0xffffffff
+#define PLACEHOLDER2 PLACEHOLDER PLACEHOLDER
+#define PLACEHOLDER4 PLACEHOLDER2 PLACEHOLDER2
+
+/dts-v1/;
+
+/ {
+ interrupt-parent = <&intc>;
+ compatible = "linux,dummy-virt";
+ #address-cells = <2>;
+ #size-cells = <2>;
+
+ chosen {
+ stdout-path = "/uart@3f8";
+ linux,pci-probe-only = <1>;
+ kaslr-seed = <PLACEHOLDER2>;
+ avf,strict-boot;
+ avf,new-instance;
+ };
+
+ memory {
+ device_type = "memory";
+ reg = <0x00 0x80000000 PLACEHOLDER2>;
+ };
+
+ reserved-memory {
+ #address-cells = <2>;
+ #size-cells = <2>;
+ ranges;
+ swiotlb: restricted_dma_reserved {
+ compatible = "restricted-dma-pool";
+ size = <PLACEHOLDER2>;
+ alignment = <PLACEHOLDER2>;
+ };
+
+ dice {
+ compatible = "google,open-dice";
+ no-map;
+ reg = <PLACEHOLDER4>;
+ };
+ };
+
+ cpus {
+ #address-cells = <1>;
+ #size-cells = <0>;
+ cpu@0 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <0>;
+ };
+ cpu@1 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <1>;
+ };
+ cpu@2 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <2>;
+ };
+ cpu@3 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <3>;
+ };
+ cpu@4 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <4>;
+ };
+ cpu@5 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <5>;
+ };
+ cpu@6 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <6>;
+ };
+ cpu@7 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <7>;
+ };
+ cpu@8 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <8>;
+ };
+ cpu@9 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <9>;
+ };
+ cpu@10 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <10>;
+ };
+ cpu@11 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <11>;
+ };
+ cpu@12 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <12>;
+ };
+ cpu@13 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <13>;
+ };
+ cpu@14 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <14>;
+ };
+ cpu@15 {
+ device_type = "cpu";
+ compatible = "arm,arm-v8";
+ enable-method = "psci";
+ reg = <15>;
+ };
+ };
+
+ intc: intc {
+ compatible = "arm,gic-v3";
+ #address-cells = <2>;
+ #size-cells = <2>;
+ #interrupt-cells = <3>;
+ interrupt-controller;
+ reg = <0x00 0x3fff0000 0x00 0x10000>, <PLACEHOLDER4>;
+ };
+
+ timer {
+ compatible = "arm,armv8-timer";
+ always-on;
+ /* The IRQ type needs to be OR-ed with the CPU mask */
+ interrupts = <GIC_PPI 0xd IRQ_TYPE_LEVEL_LOW
+ GIC_PPI 0xe IRQ_TYPE_LEVEL_LOW
+ GIC_PPI 0xb IRQ_TYPE_LEVEL_LOW
+ GIC_PPI 0xa IRQ_TYPE_LEVEL_LOW>;
+ };
+
+ uart@2e8 {
+ compatible = "ns16550a";
+ reg = <0x00 0x2e8 0x00 0x8>;
+ clock-frequency = <0x1c2000>;
+ interrupts = <GIC_SPI 2 IRQ_TYPE_EDGE_RISING>;
+ };
+
+ uart@2f8 {
+ compatible = "ns16550a";
+ reg = <0x00 0x2f8 0x00 0x8>;
+ clock-frequency = <0x1c2000>;
+ interrupts = <GIC_SPI 2 IRQ_TYPE_EDGE_RISING>;
+ };
+
+ uart@3e8 {
+ compatible = "ns16550a";
+ reg = <0x00 0x3e8 0x00 0x8>;
+ clock-frequency = <0x1c2000>;
+ interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
+ };
+
+ uart@3f8 {
+ compatible = "ns16550a";
+ reg = <0x00 0x3f8 0x00 0x8>;
+ clock-frequency = <0x1c2000>;
+ interrupts = <GIC_SPI 0 IRQ_TYPE_EDGE_RISING>;
+ };
+
+ psci {
+ compatible = "arm,psci-1.0";
+ method = "hvc";
+ };
+
+ pci {
+ compatible = "pci-host-cam-generic";
+ device_type = "pci";
+ #address-cells = <3>;
+ #size-cells = <2>;
+ #interrupt-cells = <1>;
+ dma-coherent;
+ memory-region = <&swiotlb>;
+ ranges = <
+ 0x3000000 0x0 0x02000000 0x0 0x02000000 0x00 0x02000000
+ 0x3000000 PLACEHOLDER2 PLACEHOLDER2 PLACEHOLDER2
+ >;
+ bus-range = <0x00 0x00>;
+ reg = <0x00 0x10000 0x00 0x1000000>;
+ interrupt-map = <
+ 0x0800 0x0 0x0 1 &intc 0 0 GIC_SPI 3 IRQ_TYPE_LEVEL_HIGH
+ 0x1000 0x0 0x0 1 &intc 0 0 GIC_SPI 4 IRQ_TYPE_LEVEL_HIGH
+ 0x1800 0x0 0x0 1 &intc 0 0 GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH
+ 0x2000 0x0 0x0 1 &intc 0 0 GIC_SPI 6 IRQ_TYPE_LEVEL_HIGH
+ 0x2800 0x0 0x0 1 &intc 0 0 GIC_SPI 7 IRQ_TYPE_LEVEL_HIGH
+ 0x3000 0x0 0x0 1 &intc 0 0 GIC_SPI 8 IRQ_TYPE_LEVEL_HIGH
+ 0x3800 0x0 0x0 1 &intc 0 0 GIC_SPI 9 IRQ_TYPE_LEVEL_HIGH
+ >;
+ interrupt-map-mask = <0xf800 0x0 0x0 0x7
+ 0xf800 0x0 0x0 0x7
+ 0xf800 0x0 0x0 0x7
+ 0xf800 0x0 0x0 0x7
+ 0xf800 0x0 0x0 0x7
+ 0xf800 0x0 0x0 0x7
+ 0xf800 0x0 0x0 0x7>;
+ };
+
+ clk: pclk@3M {
+ compatible = "fixed-clock";
+ clock-frequency = <0x2fefd8>;
+ #clock-cells = <0>;
+ };
+
+ rtc@2000 {
+ compatible = "arm,primecell";
+ arm,primecell-periphid = <0x41030>;
+ reg = <0x00 0x2000 0x00 0x1000>;
+ interrupts = <GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
+ clock-names = "apb_pclk";
+ clocks = <&clk>;
+ };
+
+ vmwdt@3000 {
+ compatible = "qemu,vcpu-stall-detector";
+ reg = <0x00 0x3000 0x00 0x1000>;
+ clock-frequency = <10>;
+ timeout-sec = <8>;
+ };
+};
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..275de7a
--- /dev/null
+++ b/pvmfw/src/crypto.rs
@@ -0,0 +1,300 @@
+// 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 crate::cstr;
+
+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 packed = self.packed_value();
+ let library = self.library_name().unwrap_or(cstr!("{unknown library}")).to_str().unwrap();
+ let reason = self.reason().unwrap_or(cstr!("{unknown reason}")).to_str().unwrap();
+ let file = self.file.unwrap_or(cstr!("??")).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..15efa1c 100644
--- a/pvmfw/src/debug_policy.rs
+++ b/pvmfw/src/debug_policy.rs
@@ -14,7 +14,8 @@
//! Support for the debug policy overlay in pvmfw
-use alloc::vec;
+use crate::cstr;
+use alloc::{vec, vec::Vec};
use core::ffi::CStr;
use core::fmt;
use libfdt::FdtError;
@@ -63,17 +64,10 @@
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> {
- let chosen_path = CStr::from_bytes_with_nul(b"/chosen\0").unwrap();
- let bootargs_name = CStr::from_bytes_with_nul(b"bootargs\0").unwrap();
-
+/// Disables ramdump by removing crashkernel from bootargs in /chosen.
+fn disable_ramdump(fdt: &mut libfdt::Fdt) -> Result<(), DebugPolicyError> {
let chosen = match fdt
- .node(chosen_path)
+ .node(cstr!("/chosen"))
.map_err(|e| DebugPolicyError::Fdt("Failed to find /chosen", e))?
{
Some(node) => node,
@@ -81,7 +75,7 @@
};
let bootargs = match chosen
- .getprop_str(bootargs_name)
+ .getprop_str(cstr!("bootargs"))
.map_err(|e| DebugPolicyError::Fdt("Failed to find bootargs prop", e))?
{
Some(value) if !value.to_bytes().is_empty() => value,
@@ -104,8 +98,8 @@
new_bootargs.push(b'\0');
// 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| {
+ let mut chosen_mut = fdt.node_mut(cstr!("/chosen")).unwrap().unwrap();
+ chosen_mut.setprop(cstr!("bootargs"), new_bootargs.as_slice()).map_err(|e| {
DebugPolicyError::OverlaidFdt("Failed to remove crashkernel. FDT might be corrupted", e)
})
}
@@ -113,7 +107,7 @@
/// Returns true only if fdt has ramdump prop in the /avf/guest/common node with value <1>
fn is_ramdump_enabled(fdt: &libfdt::Fdt) -> Result<bool, DebugPolicyError> {
let common = match fdt
- .node(CStr::from_bytes_with_nul(b"/avf/guest/common\0").unwrap())
+ .node(cstr!("/avf/guest/common"))
.map_err(|e| DebugPolicyError::DebugPolicyFdt("Failed to find /avf/guest/common node", e))?
{
Some(node) => node,
@@ -121,7 +115,7 @@
};
match common
- .getprop_u32(CStr::from_bytes_with_nul(b"ramdump\0").unwrap())
+ .getprop_u32(cstr!("ramdump"))
.map_err(|e| DebugPolicyError::DebugPolicyFdt("Failed to find ramdump prop", e))?
{
Some(1) => Ok(true),
@@ -129,6 +123,60 @@
}
}
+/// 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 = match fdt
+ .node(cstr!("/chosen"))
+ .map_err(|e| DebugPolicyError::Fdt("Failed to find /chosen", e))?
+ {
+ Some(node) => node,
+ None => return Ok(()),
+ };
+
+ let bootargs = match chosen
+ .getprop_str(cstr!("bootargs"))
+ .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(cstr!("/chosen")).unwrap().unwrap();
+ chosen_mut.setprop(cstr!("bootargs"), 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!("/avf/guest/common"))
+ .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!("log"))
+ .map_err(|e| DebugPolicyError::DebugPolicyFdt("Failed to find log prop", e))?
+ {
+ Some(1) => Ok(true),
+ _ => Ok(false),
+ }
+}
+
/// Handles debug policies.
///
/// # Safety
@@ -146,7 +194,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 f6a1f3d..bad3453 100644
--- a/pvmfw/src/dice.rs
+++ b/pvmfw/src/dice.rs
@@ -14,13 +14,16 @@
//! Support for DICE derivation and BCC generation.
+use crate::cstr;
+use crate::helpers::flushed_zeroize;
+use core::ffi::c_void;
use core::ffi::CStr;
use core::mem::size_of;
-use dice::bcc::Handover;
-use dice::Config;
-use dice::DiceMode;
-use dice::InputValues;
-use diced_open_dice::{bcc_format_config_descriptor, hash, HIDDEN_SIZE};
+use core::slice;
+
+use diced_open_dice::{
+ bcc_format_config_descriptor, hash, Config, DiceMode, Hash, InputValues, HIDDEN_SIZE,
+};
use pvmfw_avb::{DebugLevel, Digest, VerifiedBootData};
fn to_dice_mode(debug_level: DebugLevel) -> DiceMode {
@@ -30,7 +33,7 @@
}
}
-fn to_dice_hash(verified_boot_data: &VerifiedBootData) -> dice::Result<dice::Hash> {
+fn to_dice_hash(verified_boot_data: &VerifiedBootData) -> diced_open_dice::Result<Hash> {
let mut digests = [0u8; size_of::<Digest>() * 2];
digests[..size_of::<Digest>()].copy_from_slice(&verified_boot_data.kernel_digest);
if let Some(initrd_digest) = verified_boot_data.initrd_digest {
@@ -39,33 +42,53 @@
hash(&digests)
}
-/// Derive the VM-specific secrets and certificate through DICE.
-pub fn derive_next_bcc(
- bcc: &Handover,
- next_bcc: &mut [u8],
- verified_boot_data: &VerifiedBootData,
- authority: &[u8],
-) -> dice::Result<usize> {
- let code_hash = to_dice_hash(verified_boot_data)?;
- let auth_hash = hash(authority)?;
- let mode = to_dice_mode(verified_boot_data.debug_level);
- let component_name = CStr::from_bytes_with_nul(b"vm_entry\0").unwrap();
- let mut config_descriptor_buffer = [0; 128];
- let config_descriptor_size = bcc_format_config_descriptor(
- Some(component_name),
- None, // component_version
- false, // resettable
- &mut config_descriptor_buffer,
- )?;
- let config = &config_descriptor_buffer[..config_descriptor_size];
+pub struct PartialInputs {
+ pub code_hash: Hash,
+ pub auth_hash: Hash,
+ pub mode: DiceMode,
+}
- let input_values = InputValues::new(
- code_hash,
- Config::Descriptor(config),
- auth_hash,
- mode,
- [0u8; HIDDEN_SIZE], // TODO(b/249723852): Get salt from instance.img (virtio-blk) and/or TRNG.
- );
+impl PartialInputs {
+ pub fn new(data: &VerifiedBootData) -> diced_open_dice::Result<Self> {
+ let code_hash = to_dice_hash(data)?;
+ let auth_hash = hash(data.public_key)?;
+ let mode = to_dice_mode(data.debug_level);
- bcc.main_flow(&input_values, next_bcc)
+ Ok(Self { code_hash, auth_hash, mode })
+ }
+
+ pub fn into_input_values(
+ self,
+ salt: &[u8; HIDDEN_SIZE],
+ ) -> diced_open_dice::Result<InputValues> {
+ let mut config_descriptor_buffer = [0; 128];
+ let config_descriptor_size = bcc_format_config_descriptor(
+ Some(cstr!("vm_entry")),
+ None, // component_version
+ false, // resettable
+ &mut config_descriptor_buffer,
+ )?;
+ let config = &config_descriptor_buffer[..config_descriptor_size];
+
+ Ok(InputValues::new(
+ self.code_hash,
+ Config::Descriptor(config),
+ self.auth_hash,
+ self.mode,
+ *salt,
+ ))
+ }
+}
+
+/// Flushes data caches over the provided address range.
+///
+/// # Safety
+///
+/// The provided address and size must be to a valid address range (typically on the stack, .bss,
+/// .data, or provided BCC).
+#[no_mangle]
+unsafe extern "C" fn DiceClearMemory(_ctx: *mut c_void, size: usize, addr: *mut c_void) {
+ // SAFETY - We must trust that the slice will be valid arrays/variables on the C code stack.
+ let region = unsafe { slice::from_raw_parts_mut(addr as *mut u8, size) };
+ flushed_zeroize(region)
}
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index 530449c..106a4ef 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -22,10 +22,10 @@
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;
-use dice::bcc::Handover;
use log::debug;
use log::error;
use log::info;
@@ -242,11 +242,7 @@
RebootReason::InvalidConfig
})?;
- let bcc_slice = appended.get_bcc_mut();
- let bcc = Handover::new(bcc_slice).map_err(|e| {
- error!("Invalid BCC Handover: {e:?}");
- RebootReason::InvalidBcc
- })?;
+ 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
@@ -257,15 +253,20 @@
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, &mut memory)?;
+ crate::main(slices.fdt, slices.kernel, slices.ramdisk, bcc_slice, &mut memory)?;
helpers::flushed_zeroize(bcc_slice);
helpers::flush(slices.fdt.as_slice());
// 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)
})?;
@@ -396,18 +397,10 @@
}
}
- #[allow(dead_code)] // TODO(b/232900974)
- 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..f56d6e0 100644
--- a/pvmfw/src/fdt.rs
+++ b/pvmfw/src/fdt.rs
@@ -14,16 +14,27 @@
//! High-level FDT functions.
+use crate::cstr;
+use crate::helpers::GUEST_PAGE_SIZE;
+use crate::RebootReason;
use core::ffi::CStr;
+use core::num::NonZeroUsize;
use core::ops::Range;
+use fdtpci::PciMemoryFlags;
+use fdtpci::PciRangeType;
+use libfdt::AddressRange;
+use libfdt::CellIterator;
+use libfdt::Fdt;
+use libfdt::FdtError;
+use log::error;
+use tinyvec::ArrayVec;
/// Extract from /config the address range containing the pre-loaded kernel.
pub fn kernel_range(fdt: &libfdt::Fdt) -> libfdt::Result<Option<Range<usize>>> {
- let config = CStr::from_bytes_with_nul(b"/config\0").unwrap();
- let addr = CStr::from_bytes_with_nul(b"kernel-address\0").unwrap();
- let size = CStr::from_bytes_with_nul(b"kernel-size\0").unwrap();
+ let addr = cstr!("kernel-address");
+ let size = cstr!("kernel-size");
- if let Some(config) = fdt.node(config)? {
+ if let Some(config) = fdt.node(cstr!("/config"))? {
if let (Some(addr), Some(size)) = (config.getprop_u32(addr)?, config.getprop_u32(size)?) {
let addr = addr as usize;
let size = size as usize;
@@ -37,8 +48,8 @@
/// Extract from /chosen the address range containing the pre-loaded ramdisk.
pub fn initrd_range(fdt: &libfdt::Fdt) -> libfdt::Result<Option<Range<usize>>> {
- let start = CStr::from_bytes_with_nul(b"linux,initrd-start\0").unwrap();
- let end = CStr::from_bytes_with_nul(b"linux,initrd-end\0").unwrap();
+ let start = cstr!("linux,initrd-start");
+ let end = cstr!("linux,initrd-end");
if let Some(chosen) = fdt.chosen()? {
if let (Some(start), Some(end)) = (chosen.getprop_u32(start)?, chosen.getprop_u32(end)?) {
@@ -49,28 +60,454 @@
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<()> {
+/// Read and validate the size and base address of memory, and returns the size
+fn parse_memory_node(fdt: &libfdt::Fdt) -> Result<NonZeroUsize, RebootReason> {
+ let memory_range = fdt
+ .memory()
+ // Actually, these checks are unnecessary because we read /memory node in entry.rs
+ // where the exactly same checks are done. We are repeating the same check just for
+ // extra safety (in case when the code structure changes in the future).
+ .map_err(|e| {
+ error!("Failed to get /memory from the DT: {e}");
+ RebootReason::InvalidFdt
+ })?
+ .ok_or_else(|| {
+ error!("Node /memory was found empty");
+ RebootReason::InvalidFdt
+ })?
+ .next()
+ .ok_or_else(|| {
+ error!("Failed to read memory range from the DT");
+ RebootReason::InvalidFdt
+ })?;
+
+ let base = memory_range.start;
+ if base as u64 != DeviceTreeInfo::RAM_BASE_ADDR {
+ error!("Memory base address {:#x} is not {:#x}", base, DeviceTreeInfo::RAM_BASE_ADDR);
+ return Err(RebootReason::InvalidFdt);
+ }
+
+ let size = memory_range.len(); // end is exclusive
+ if size % GUEST_PAGE_SIZE != 0 {
+ error!("Memory size {:#x} is not a multiple of page size {:#x}", size, GUEST_PAGE_SIZE);
+ return Err(RebootReason::InvalidFdt);
+ }
+ // In the u-boot implementation, we checked if base + size > u64::MAX, but we don't need that
+ // because memory() function uses checked_add when constructing the Range object. If an
+ // overflow happened, we should have gotten None from the next() call above and would have
+ // bailed already.
+
+ NonZeroUsize::new(size).ok_or_else(|| {
+ error!("Memory size can't be 0");
+ RebootReason::InvalidFdt
+ })
+}
+
+/// Read the number of CPUs
+fn parse_cpu_nodes(fdt: &libfdt::Fdt) -> Result<NonZeroUsize, RebootReason> {
+ let num = fdt
+ .compatible_nodes(cstr!("arm,arm-v8"))
+ .map_err(|e| {
+ error!("Failed to read compatible nodes \"arm,arm-v8\" from DT: {e}");
+ RebootReason::InvalidFdt
+ })?
+ .count();
+ NonZeroUsize::new(num).ok_or_else(|| {
+ error!("Number of CPU can't be 0");
+ RebootReason::InvalidFdt
+ })
+}
+
+#[derive(Debug)]
+#[allow(dead_code)] // TODO: remove this
+struct PciInfo {
+ ranges: [Range<u64>; 2],
+ num_irq: usize,
+}
+
+/// Read and validate PCI node
+fn parse_pci_nodes(fdt: &libfdt::Fdt) -> Result<PciInfo, RebootReason> {
+ let node = fdt
+ .compatible_nodes(cstr!("pci-host-cam-generic"))
+ .map_err(|e| {
+ error!("Failed to read compatible node \"pci-host-cam-generic\" from DT: {e}");
+ RebootReason::InvalidFdt
+ })?
+ .next()
+ .ok_or_else(|| {
+ // pvmfw requires at least one pci device (virtio-blk) for the instance disk. So,
+ // let's fail early.
+ error!("Compatible node \"pci-host-cam-generic\" doesn't exist");
+ RebootReason::InvalidFdt
+ })?;
+
+ let mut iter = node
+ .ranges::<(u32, u64), u64, u64>()
+ .map_err(|e| {
+ error!("Failed to read ranges from PCI node: {e}");
+ RebootReason::InvalidFdt
+ })?
+ .ok_or_else(|| {
+ error!("PCI node missing ranges property");
+ RebootReason::InvalidFdt
+ })?;
+
+ let range0 = iter.next().ok_or_else(|| {
+ error!("First range missing in PCI node");
+ RebootReason::InvalidFdt
+ })?;
+ let range0 = get_and_validate_pci_range(&range0)?;
+
+ let range1 = iter.next().ok_or_else(|| {
+ error!("Second range missing in PCI node");
+ RebootReason::InvalidFdt
+ })?;
+ let range1 = get_and_validate_pci_range(&range1)?;
+
+ let num_irq = count_and_validate_pci_irq_masks(&node)?;
+
+ validate_pci_irq_maps(&node)?;
+
+ Ok(PciInfo { ranges: [range0, range1], num_irq })
+}
+
+fn get_and_validate_pci_range(
+ range: &AddressRange<(u32, u64), u64, u64>,
+) -> Result<Range<u64>, RebootReason> {
+ let mem_flags = PciMemoryFlags(range.addr.0);
+ let range_type = mem_flags.range_type();
+ let prefetchable = mem_flags.prefetchable();
+ let bus_addr = range.addr.1;
+ let cpu_addr = range.parent_addr;
+ let size = range.size;
+ if range_type != PciRangeType::Memory64 {
+ error!("Invalid range type {:?} for bus address {:#x} in PCI node", range_type, bus_addr);
+ return Err(RebootReason::InvalidFdt);
+ }
+ if prefetchable {
+ error!("PCI bus address {:#x} in PCI node is prefetchable", bus_addr);
+ return Err(RebootReason::InvalidFdt);
+ }
+ // Enforce ID bus-to-cpu mappings, as used by crosvm.
+ if bus_addr != cpu_addr {
+ error!("PCI bus address: {:#x} is different from CPU address: {:#x}", bus_addr, cpu_addr);
+ return Err(RebootReason::InvalidFdt);
+ }
+ let bus_end = bus_addr.checked_add(size).ok_or_else(|| {
+ error!("PCI address range size {:#x} too big", size);
+ RebootReason::InvalidFdt
+ })?;
+ Ok(bus_addr..bus_end)
+}
+
+/// Iterator that takes N cells as a chunk
+struct CellChunkIterator<'a, const N: usize> {
+ cells: CellIterator<'a>,
+}
+
+impl<'a, const N: usize> CellChunkIterator<'a, N> {
+ fn new(cells: CellIterator<'a>) -> Self {
+ Self { cells }
+ }
+}
+
+impl<'a, const N: usize> Iterator for CellChunkIterator<'a, N> {
+ type Item = [u32; N];
+ fn next(&mut self) -> Option<Self::Item> {
+ let mut ret: Self::Item = [0; N];
+ for i in ret.iter_mut() {
+ *i = self.cells.next()?;
+ }
+ Some(ret)
+ }
+}
+
+fn count_and_validate_pci_irq_masks(pci_node: &libfdt::FdtNode) -> Result<usize, RebootReason> {
+ const IRQ_MASK_CELLS: usize = 4;
+ const IRQ_MASK_ADDR_HI: u32 = 0xf800;
+ const IRQ_MASK_ADDR_ME: u32 = 0x0;
+ const IRQ_MASK_ADDR_LO: u32 = 0x0;
+ const IRQ_MASK_ANY_IRQ: u32 = 0x7;
+ const EXPECTED: [u32; IRQ_MASK_CELLS] =
+ [IRQ_MASK_ADDR_HI, IRQ_MASK_ADDR_ME, IRQ_MASK_ADDR_LO, IRQ_MASK_ANY_IRQ];
+
+ let mut irq_count: usize = 0;
+ for irq_mask in CellChunkIterator::<IRQ_MASK_CELLS>::new(
+ pci_node
+ .getprop_cells(cstr!("interrupt-map-mask"))
+ .map_err(|e| {
+ error!("Failed to read interrupt-map-mask property: {e}");
+ RebootReason::InvalidFdt
+ })?
+ .ok_or_else(|| {
+ error!("PCI node missing interrupt-map-mask property");
+ RebootReason::InvalidFdt
+ })?,
+ ) {
+ if irq_mask != EXPECTED {
+ error!("invalid irq mask {:?}", irq_mask);
+ return Err(RebootReason::InvalidFdt);
+ }
+ irq_count += 1;
+ }
+ Ok(irq_count)
+}
+
+fn validate_pci_irq_maps(pci_node: &libfdt::FdtNode) -> Result<(), RebootReason> {
+ const IRQ_MAP_CELLS: usize = 10;
+ const PCI_DEVICE_IDX: usize = 11;
+ const PCI_IRQ_ADDR_ME: u32 = 0;
+ const PCI_IRQ_ADDR_LO: u32 = 0;
+ const PCI_IRQ_INTC: u32 = 1;
+ const AARCH64_IRQ_BASE: u32 = 4; // from external/crosvm/aarch64/src/lib.rs
+ const GIC_SPI: u32 = 0;
+ const IRQ_TYPE_LEVEL_HIGH: u32 = 4;
+
+ let mut phys_hi: u32 = 0;
+ let mut irq_nr = AARCH64_IRQ_BASE;
+
+ for irq_map in CellChunkIterator::<IRQ_MAP_CELLS>::new(
+ pci_node
+ .getprop_cells(cstr!("interrupt-map"))
+ .map_err(|e| {
+ error!("Failed to read interrupt-map property: {e}");
+ RebootReason::InvalidFdt
+ })?
+ .ok_or_else(|| {
+ error!("PCI node missing interrupt-map property");
+ RebootReason::InvalidFdt
+ })?,
+ ) {
+ phys_hi += 0x1 << PCI_DEVICE_IDX;
+
+ let pci_addr = (irq_map[0], irq_map[1], irq_map[2]);
+ let pci_irq_number = irq_map[3];
+ let _controller_phandle = irq_map[4]; // skipped.
+ let gic_addr = (irq_map[5], irq_map[6]); // address-cells is <2> for GIC
+ // interrupt-cells is <3> for GIC
+ let gic_peripheral_interrupt_type = irq_map[7];
+ let gic_irq_number = irq_map[8];
+ let gic_irq_type = irq_map[9];
+
+ let expected_pci_addr = (phys_hi, PCI_IRQ_ADDR_ME, PCI_IRQ_ADDR_LO);
+
+ if pci_addr != expected_pci_addr {
+ error!("PCI device address {:#x} {:#x} {:#x} in interrupt-map is different from expected address \
+ {:#x} {:#x} {:#x}",
+ pci_addr.0, pci_addr.1, pci_addr.2, expected_pci_addr.0, expected_pci_addr.1, expected_pci_addr.2);
+ return Err(RebootReason::InvalidFdt);
+ }
+ if pci_irq_number != PCI_IRQ_INTC {
+ error!(
+ "PCI INT# {:#x} in interrupt-map is different from expected value {:#x}",
+ pci_irq_number, PCI_IRQ_INTC
+ );
+ return Err(RebootReason::InvalidFdt);
+ }
+ if gic_addr != (0, 0) {
+ error!(
+ "GIC address {:#x} {:#x} in interrupt-map is different from expected address \
+ {:#x} {:#x}",
+ gic_addr.0, gic_addr.1, 0, 0
+ );
+ return Err(RebootReason::InvalidFdt);
+ }
+ if gic_peripheral_interrupt_type != GIC_SPI {
+ error!("GIC peripheral interrupt type {:#x} in interrupt-map is different from expected value \
+ {:#x}", gic_peripheral_interrupt_type, GIC_SPI);
+ return Err(RebootReason::InvalidFdt);
+ }
+ if gic_irq_number != irq_nr {
+ error!(
+ "GIC irq number {:#x} in interrupt-map is unexpected. Expected {:#x}",
+ gic_irq_number, irq_nr
+ );
+ return Err(RebootReason::InvalidFdt);
+ }
+ irq_nr += 1; // move to next irq
+ if gic_irq_type != IRQ_TYPE_LEVEL_HIGH {
+ error!(
+ "IRQ type in {:#x} is invalid. Must be LEVEL_HIGH {:#x}",
+ gic_irq_type, IRQ_TYPE_LEVEL_HIGH
+ );
+ return Err(RebootReason::InvalidFdt);
+ }
+ }
+ Ok(())
+}
+
+#[derive(Default, Debug)]
+#[allow(dead_code)] // TODO: remove this
+pub struct SerialInfo {
+ addrs: ArrayVec<[u64; Self::SERIAL_MAX_COUNT]>,
+}
+
+impl SerialInfo {
+ const SERIAL_MAX_COUNT: usize = 4;
+}
+
+fn parse_serial_nodes(fdt: &libfdt::Fdt) -> Result<SerialInfo, RebootReason> {
+ let mut ret: SerialInfo = Default::default();
+ for (i, node) in fdt
+ .compatible_nodes(cstr!("ns16550a"))
+ .map_err(|e| {
+ error!("Failed to read compatible nodes \"ns16550a\" from DT: {e}");
+ RebootReason::InvalidFdt
+ })?
+ .enumerate()
+ {
+ if i >= ret.addrs.capacity() {
+ error!("Too many serials: {i}");
+ return Err(RebootReason::InvalidFdt);
+ }
+ let reg = node
+ .reg()
+ .map_err(|e| {
+ error!("Failed to read reg property from \"ns16550a\" node: {e}");
+ RebootReason::InvalidFdt
+ })?
+ .ok_or_else(|| {
+ error!("No reg property in \"ns16550a\" node");
+ RebootReason::InvalidFdt
+ })?
+ .next()
+ .ok_or_else(|| {
+ error!("No value in reg property of \"ns16550a\" node");
+ RebootReason::InvalidFdt
+ })?;
+ ret.addrs.push(reg.addr);
+ }
+ Ok(ret)
+}
+
+#[derive(Debug)]
+#[allow(dead_code)] // TODO: remove this
+pub struct SwiotlbInfo {
+ size: u64,
+ align: u64,
+}
+
+fn parse_swiotlb_nodes(fdt: &libfdt::Fdt) -> Result<SwiotlbInfo, RebootReason> {
+ let node = fdt
+ .compatible_nodes(cstr!("restricted-dma-pool"))
+ .map_err(|e| {
+ error!("Failed to read compatible nodes \"restricted-dma-pool\" from DT: {e}");
+ RebootReason::InvalidFdt
+ })?
+ .next()
+ .ok_or_else(|| {
+ error!("No compatible node \"restricted-dma-pool\" in DT");
+ RebootReason::InvalidFdt
+ })?;
+ let size = node
+ .getprop_u64(cstr!("size"))
+ .map_err(|e| {
+ error!("Failed to read \"size\" property of \"restricted-dma-pool\": {e}");
+ RebootReason::InvalidFdt
+ })?
+ .ok_or_else(|| {
+ error!("No \"size\" property in \"restricted-dma-pool\"");
+ RebootReason::InvalidFdt
+ })?;
+
+ let align = node
+ .getprop_u64(cstr!("alignment"))
+ .map_err(|e| {
+ error!("Failed to read \"alignment\" property of \"restricted-dma-pool\": {e}");
+ RebootReason::InvalidFdt
+ })?
+ .ok_or_else(|| {
+ error!("No \"alignment\" property in \"restricted-dma-pool\"");
+ RebootReason::InvalidFdt
+ })?;
+
+ if size == 0 || (size % GUEST_PAGE_SIZE as u64) != 0 {
+ error!("Invalid swiotlb size {:#x}", size);
+ return Err(RebootReason::InvalidFdt);
+ }
+
+ if (align % GUEST_PAGE_SIZE as u64) != 0 {
+ error!("Invalid swiotlb alignment {:#x}", align);
+ return Err(RebootReason::InvalidFdt);
+ }
+
+ Ok(SwiotlbInfo { size, align })
+}
+
+#[derive(Debug)]
+#[allow(dead_code)] // TODO: remove this
+pub struct DeviceTreeInfo {
+ memory_size: NonZeroUsize,
+ num_cpu: NonZeroUsize,
+ pci_info: PciInfo,
+ serial_info: SerialInfo,
+ swiotlb_info: SwiotlbInfo,
+}
+
+impl DeviceTreeInfo {
+ const RAM_BASE_ADDR: u64 = 0x8000_0000;
+}
+
+pub fn parse_device_tree(fdt: &libfdt::Fdt) -> Result<DeviceTreeInfo, RebootReason> {
+ Ok(DeviceTreeInfo {
+ memory_size: parse_memory_node(fdt)?,
+ num_cpu: parse_cpu_nodes(fdt)?,
+ pci_info: parse_pci_nodes(fdt)?,
+ serial_info: parse_serial_nodes(fdt)?,
+ swiotlb_info: parse_swiotlb_nodes(fdt)?,
+ })
+}
+
+/// 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()?;
- 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.
- let mut reserved_memory = fdt.node_mut(reserved_memory)?.ok_or(libfdt::FdtError::NotFound)?;
+ add_dice_node(fdt, bcc.as_ptr() as usize, bcc.len())?;
- let dice = CStr::from_bytes_with_nul(b"dice\0").unwrap();
- let mut dice = reserved_memory.add_subnode(dice)?;
-
- let compatible = CStr::from_bytes_with_nul(b"compatible\0").unwrap();
- dice.appendprop(compatible, b"google,open-dice\0")?;
-
- let no_map = CStr::from_bytes_with_nul(b"no-map\0").unwrap();
- dice.appendprop(no_map, &[])?;
-
- let reg = CStr::from_bytes_with_nul(b"reg\0").unwrap();
- dice.appendprop_addrrange(reg, addr as u64, size as u64)?;
+ set_or_clear_chosen_flag(fdt, cstr!("avf,strict-boot"), strict_boot)?;
+ set_or_clear_chosen_flag(fdt, cstr!("avf,new-instance"), 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<()> {
+ // We reject DTs with missing reserved-memory node as validation should have checked that the
+ // "swiotlb" subnode (compatible = "restricted-dma-pool") was present.
+ let mut reserved_memory =
+ fdt.node_mut(cstr!("/reserved-memory"))?.ok_or(libfdt::FdtError::NotFound)?;
+
+ let mut dice = reserved_memory.add_subnode(cstr!("dice"))?;
+
+ dice.appendprop(cstr!("compatible"), b"google,open-dice\0")?;
+
+ dice.appendprop(cstr!("no-map"), &[])?;
+
+ let addr = addr.try_into().unwrap();
+ let size = size.try_into().unwrap();
+ dice.appendprop_addrrange(cstr!("reg"), addr, size)?;
+
+ 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/heap.rs b/pvmfw/src/heap.rs
index 435a6ff..eea2e98 100644
--- a/pvmfw/src/heap.rs
+++ b/pvmfw/src/heap.rs
@@ -53,7 +53,15 @@
#[no_mangle]
unsafe extern "C" fn malloc(size: usize) -> *mut c_void {
- malloc_(size).map_or(ptr::null_mut(), |p| p.cast::<c_void>().as_ptr())
+ malloc_(size, false).map_or(ptr::null_mut(), |p| p.cast::<c_void>().as_ptr())
+}
+
+#[no_mangle]
+unsafe extern "C" fn calloc(nmemb: usize, size: usize) -> *mut c_void {
+ let Some(size) = nmemb.checked_mul(size) else {
+ return ptr::null_mut()
+ };
+ malloc_(size, true).map_or(ptr::null_mut(), |p| p.cast::<c_void>().as_ptr())
}
#[no_mangle]
@@ -67,9 +75,11 @@
}
}
-unsafe fn malloc_(size: usize) -> Option<NonNull<usize>> {
+unsafe fn malloc_(size: usize, zeroed: bool) -> Option<NonNull<usize>> {
let size = NonZeroUsize::new(size)?.checked_add(mem::size_of::<usize>())?;
- let ptr = HEAP_ALLOCATOR.alloc(malloc_layout(size)?);
+ let layout = malloc_layout(size)?;
+ let ptr =
+ if zeroed { HEAP_ALLOCATOR.alloc_zeroed(layout) } else { HEAP_ALLOCATOR.alloc(layout) };
let ptr = NonNull::new(ptr)?.cast::<usize>().as_ptr();
*ptr = size.get();
NonNull::new(ptr.offset(1))
diff --git a/pvmfw/src/helpers.rs b/pvmfw/src/helpers.rs
index 40266f7..fddd8c3 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.
@@ -102,3 +113,11 @@
reg.zeroize();
flush(reg)
}
+
+/// Create &CStr out of &str literal
+#[macro_export]
+macro_rules! cstr {
+ ($str:literal) => {{
+ CStr::from_bytes_with_nul(concat!($str, "\0").as_bytes()).unwrap()
+ }};
+}
diff --git a/pvmfw/src/hvc.rs b/pvmfw/src/hvc.rs
index dc99303..08edd86 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;
@@ -28,14 +38,14 @@
/// Queries the memory protection parameters for a protected virtual machine.
///
/// Returns the memory protection granule size in bytes.
-pub fn hyp_meminfo() -> smccc::Result<u64> {
+pub fn kvm_hyp_meminfo() -> smccc::Result<u64> {
let args = [0u64; 17];
checked_hvc64(ARM_SMCCC_KVM_FUNC_HYP_MEMINFO, args)
}
/// Shares a region of memory with the KVM host, granting it read, write and execute permissions.
/// The size of the region is equal to the memory protection granule returned by [`hyp_meminfo`].
-pub fn mem_share(base_ipa: u64) -> smccc::Result<()> {
+pub fn kvm_mem_share(base_ipa: u64) -> smccc::Result<()> {
let mut args = [0u64; 17];
args[0] = base_ipa;
@@ -45,26 +55,26 @@
/// Revokes access permission from the KVM host to a memory region previously shared with
/// [`mem_share`]. The size of the region is equal to the memory protection granule returned by
/// [`hyp_meminfo`].
-pub fn mem_unshare(base_ipa: u64) -> smccc::Result<()> {
+pub fn kvm_mem_unshare(base_ipa: u64) -> smccc::Result<()> {
let mut args = [0u64; 17];
args[0] = base_ipa;
checked_hvc64_expect_zero(ARM_SMCCC_KVM_FUNC_MEM_UNSHARE, args)
}
-pub fn mmio_guard_info() -> smccc::Result<u64> {
+pub fn kvm_mmio_guard_info() -> smccc::Result<u64> {
let args = [0u64; 17];
checked_hvc64(VENDOR_HYP_KVM_MMIO_GUARD_INFO_FUNC_ID, args)
}
-pub fn mmio_guard_enroll() -> smccc::Result<()> {
+pub fn kvm_mmio_guard_enroll() -> smccc::Result<()> {
let args = [0u64; 17];
checked_hvc64_expect_zero(VENDOR_HYP_KVM_MMIO_GUARD_ENROLL_FUNC_ID, args)
}
-pub fn mmio_guard_map(ipa: u64) -> smccc::Result<()> {
+pub fn kvm_mmio_guard_map(ipa: u64) -> smccc::Result<()> {
let mut args = [0u64; 17];
args[0] = ipa;
@@ -84,7 +94,7 @@
}
}
-pub fn mmio_guard_unmap(ipa: u64) -> smccc::Result<()> {
+pub fn kvm_mmio_guard_unmap(ipa: u64) -> smccc::Result<()> {
let mut args = [0u64; 17];
args[0] = ipa;
@@ -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/hypervisor.rs b/pvmfw/src/hypervisor.rs
new file mode 100644
index 0000000..e06d809
--- /dev/null
+++ b/pvmfw/src/hypervisor.rs
@@ -0,0 +1,46 @@
+// 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.
+
+//! Wrappers around hypervisor back-ends.
+
+use crate::hvc;
+use crate::smccc;
+
+pub fn hyp_meminfo() -> smccc::Result<u64> {
+ hvc::kvm_hyp_meminfo()
+}
+
+pub fn mem_share(base_ipa: u64) -> smccc::Result<()> {
+ hvc::kvm_mem_share(base_ipa)
+}
+
+pub fn mem_unshare(base_ipa: u64) -> smccc::Result<()> {
+ hvc::kvm_mem_unshare(base_ipa)
+}
+
+pub fn mmio_guard_info() -> smccc::Result<u64> {
+ hvc::kvm_mmio_guard_info()
+}
+
+pub fn mmio_guard_enroll() -> smccc::Result<()> {
+ hvc::kvm_mmio_guard_enroll()
+}
+
+pub fn mmio_guard_map(ipa: u64) -> smccc::Result<()> {
+ hvc::kvm_mmio_guard_map(ipa)
+}
+
+pub fn mmio_guard_unmap(ipa: u64) -> smccc::Result<()> {
+ hvc::kvm_mmio_guard_unmap(ipa)
+}
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 be5a16a..e1ecac4 100644
--- a/pvmfw/src/main.rs
+++ b/pvmfw/src/main.rs
@@ -21,32 +21,39 @@
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 hypervisor;
+mod instance;
mod memory;
mod mmio_guard;
mod mmu;
+mod rand;
mod smccc;
mod virtio;
use alloc::boxed::Box;
-use crate::{
- dice::derive_next_bcc,
- entry::RebootReason,
- fdt::add_dice_node,
- helpers::flush,
- helpers::GUEST_PAGE_SIZE,
- memory::MemoryTracker,
- virtio::pci::{self, find_virtio_devices},
-};
-use ::dice::bcc;
+use crate::dice::PartialInputs;
+use crate::entry::RebootReason;
+use crate::fdt::modify_for_next_stage;
+use crate::fdt::parse_device_tree;
+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};
@@ -59,11 +66,11 @@
fdt: &mut Fdt,
signed_kernel: &[u8],
ramdisk: Option<&[u8]>,
- bcc: &bcc::Handover,
+ current_bcc_handover: &[u8],
memory: &mut MemoryTracker,
) -> Result<(), RebootReason> {
info!("pVM firmware");
- debug!("FDT: {:?}", fdt as *const libfdt::Fdt);
+ debug!("FDT: {:?}", fdt.as_ptr());
debug!("Signed kernel: {:?} ({:#x} bytes)", signed_kernel.as_ptr(), signed_kernel.len());
debug!("AVB public key: addr={:?}, size={:#x} ({1})", PUBLIC_KEY.as_ptr(), PUBLIC_KEY.len());
if let Some(rd) = ramdisk {
@@ -71,13 +78,21 @@
} else {
debug!("Ramdisk: None");
}
- trace!("BCC: {bcc:x?}");
+ let bcc_handover = bcc_handover_parse(current_bcc_handover).map_err(|e| {
+ error!("Invalid BCC Handover: {e:?}");
+ RebootReason::InvalidBcc
+ })?;
+ trace!("BCC: {bcc_handover:x?}");
+
+ // This parsing step includes validation. So this effectively ensures that the DT can't be
+ // abused by the host to attack pvmfw in pci::initialize below.
+ let device_tree_info = parse_device_tree(fdt)?;
+ debug!("Device tree info: {:?}", device_tree_info);
// Set up PCI bus for VirtIO devices.
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}");
@@ -90,17 +105,32 @@
})?;
// By leaking the slice, its content will be left behind for the next stage.
let next_bcc = Box::leak(next_bcc);
- let next_bcc_size =
- derive_next_bcc(bcc, next_bcc, &verified_boot_data, PUBLIC_KEY).map_err(|e| {
- error!("Failed to derive next-stage DICE secrets: {e:?}");
- RebootReason::SecretDerivationError
- })?;
- trace!("Next BCC: {:x?}", bcc::Handover::new(&next_bcc[..next_bcc_size]));
+ let dice_inputs = PartialInputs::new(&verified_boot_data).map_err(|e| {
+ error!("Failed to compute partial DICE inputs: {e:?}");
+ RebootReason::InternalError
+ })?;
+ 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
+ })?;
+ let _ = bcc_handover_main_flow(current_bcc_handover, &dice_inputs, next_bcc).map_err(|e| {
+ error!("Failed to derive next-stage DICE secrets: {e:?}");
+ RebootReason::SecretDerivationError
+ })?;
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/memory.rs b/pvmfw/src/memory.rs
index 7eecb97..17dd36b 100644
--- a/pvmfw/src/memory.rs
+++ b/pvmfw/src/memory.rs
@@ -17,7 +17,7 @@
#![deny(unsafe_op_in_unsafe_fn)]
use crate::helpers::{self, align_down, align_up, page_4kb_of, SIZE_4KB};
-use crate::hvc::{hyp_meminfo, mem_share, mem_unshare};
+use crate::hypervisor::{hyp_meminfo, mem_share, mem_unshare};
use crate::mmio_guard;
use crate::mmu;
use crate::smccc;
@@ -267,7 +267,7 @@
for region in &self.regions {
match region.mem_type {
MemoryType::ReadWrite => {
- // TODO: Use page table's dirty bit to only flush pages that were touched.
+ // TODO(b/269738062): Use PT's dirty bit to only flush pages that were touched.
helpers::flush_region(region.range.start, region.range.len())
}
MemoryType::ReadOnly => {}
@@ -314,10 +314,7 @@
// non-zero size.
let buffer = unsafe { alloc_zeroed(layout) };
- // TODO: Use let-else once we have Rust 1.65 in AOSP.
- let buffer = if let Some(buffer) = NonNull::new(buffer) {
- buffer
- } else {
+ let Some(buffer) = NonNull::new(buffer) else {
handle_alloc_error(layout);
};
diff --git a/pvmfw/src/mmio_guard.rs b/pvmfw/src/mmio_guard.rs
index e5f376e..dac26e0 100644
--- a/pvmfw/src/mmio_guard.rs
+++ b/pvmfw/src/mmio_guard.rs
@@ -15,7 +15,7 @@
//! Safe MMIO_GUARD support.
use crate::helpers;
-use crate::hvc::{mmio_guard_enroll, mmio_guard_info, mmio_guard_map, mmio_guard_unmap};
+use crate::hypervisor::{mmio_guard_enroll, mmio_guard_info, mmio_guard_map, mmio_guard_unmap};
use crate::smccc;
use core::{fmt, result};
diff --git a/pvmfw/src/rand.rs b/pvmfw/src/rand.rs
new file mode 100644
index 0000000..bf0edd5
--- /dev/null
+++ b/pvmfw/src/rand.rs
@@ -0,0 +1,109 @@
+// 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 (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) = repeat_trng_rnd(chunk.len())?;
+
+ 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) = repeat_trng_rnd(remainder.len())?;
+
+ 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(())
+}
+
+fn repeat_trng_rnd(n_bytes: usize) -> hvc::trng::Result<hvc::TrngRng64Entropy> {
+ let bits = usize::try_from(u8::BITS).unwrap();
+ let n_bits = (n_bytes * bits).try_into().unwrap();
+ loop {
+ match hvc::trng_rnd64(n_bits) {
+ Err(hvc::trng::Error::NoEntropy) => continue,
+ res => return res,
+ }
+ }
+}
+
+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 d3b3124..58bc07e 100644
--- a/pvmfw/src/virtio/pci.rs
+++ b/pvmfw/src/virtio/pci.rs
@@ -17,13 +17,16 @@
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::PciRoot, virtio_device_type, PciTransport},
+ pci::{
+ bus::{BusDeviceIterator, PciRoot},
+ virtio_device_type, PciTransport,
+ },
DeviceType, Transport,
},
};
@@ -66,31 +69,48 @@
Ok(())
}
-/// Finds VirtIO PCI devices.
-pub fn find_virtio_devices(pci_root: &mut PciRoot) -> Result<(), PciError> {
- for (device_function, info) in pci_root.enumerate_bus(0) {
- let (status, command) = pci_root.get_status_command(device_function);
- debug!(
- "Found PCI device {} at {}, status {:?} command {:?}",
- info, device_function, status, command
- );
- if let Some(virtio_type) = virtio_device_type(&info) {
+pub type VirtIOBlk = blk::VirtIOBlk<HalImpl, PciTransport>;
+
+pub struct VirtIOBlkIterator<'a> {
+ pci_root: &'a mut PciRoot,
+ bus: BusDeviceIterator,
+}
+
+impl<'a> VirtIOBlkIterator<'a> {
+ pub fn new(pci_root: &'a mut PciRoot) -> Self {
+ let bus = pci_root.enumerate_bus(0);
+ Self { pci_root, bus }
+ }
+}
+
+impl<'a> Iterator for VirtIOBlkIterator<'a> {
+ type Item = VirtIOBlk;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ let (device_function, info) = self.bus.next()?;
+ let (status, command) = self.pci_root.get_status_command(device_function);
+ debug!(
+ "Found PCI device {} at {}, status {:?} command {:?}",
+ info, device_function, status, command
+ );
+
+ let Some(virtio_type) = virtio_device_type(&info) else {
+ continue;
+ };
debug!(" VirtIO {:?}", virtio_type);
- let mut transport = PciTransport::new::<HalImpl>(pci_root, device_function).unwrap();
- info!(
+
+ let mut transport =
+ PciTransport::new::<HalImpl>(self.pci_root, device_function).unwrap();
+ debug!(
"Detected virtio PCI device with device type {:?}, features {:#018x}",
transport.device_type(),
transport.read_device_features(),
);
+
if virtio_type == DeviceType::Block {
- let mut blk =
- VirtIOBlk::<HalImpl, _>::new(transport).expect("failed to create blk driver");
- 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");
+ return Some(Self::Item::new(transport).expect("failed to create blk driver"));
}
}
}
-
- Ok(())
}
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index b25034f..be5f118 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -16,7 +16,7 @@
use android_system_virtualizationservice::{
aidl::android::system::virtualizationservice::{
- VirtualMachineConfig::VirtualMachineConfig,
+ CpuTopology::CpuTopology, VirtualMachineConfig::VirtualMachineConfig,
VirtualMachineRawConfig::VirtualMachineRawConfig,
},
binder::{ParcelFileDescriptor, ProcessState},
@@ -65,9 +65,10 @@
disks: vec![],
protectedVm: false,
memoryMib: 300,
- numCpus: 1,
+ cpuTopology: CpuTopology::ONE_CPU,
platformVersion: "~1.0".to_string(),
taskProfiles: vec![],
+ gdbPort: 0, // No gdb
});
let vm = VmInstance::create(service.as_ref(), &config, Some(console), Some(log), None)
.context("Failed to create VM")?;
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/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/libs/dice/src/lib.rs b/tests/aidl/com/android/microdroid/testservice/IVmCallback.aidl
similarity index 62%
copy from libs/dice/src/lib.rs
copy to tests/aidl/com/android/microdroid/testservice/IVmCallback.aidl
index 6870eeb..617d184 100644
--- a/libs/dice/src/lib.rs
+++ b/tests/aidl/com/android/microdroid/testservice/IVmCallback.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * 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.
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.microdroid.testservice;
-//! Bare metal wrapper around libopen_dice.
-
-#![no_std]
-
-pub use diced_open_dice::{
- bcc_format_config_descriptor, check_result, Cdi, Config, DiceError, DiceMode, Hash,
- InputValues, Result, CDI_SIZE, HASH_SIZE, HIDDEN_SIZE,
-};
-
-pub mod bcc;
+/**
+ * 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 db87126..4b11d77 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -16,6 +16,8 @@
package com.android.microdroid.benchmark;
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
@@ -27,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;
@@ -38,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;
@@ -46,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;
@@ -69,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";
@@ -190,21 +202,21 @@
@Test
public void testMicrodroidBootTime()
throws VirtualMachineException, InterruptedException, IOException {
- BootTimeStats stats = runBootTimeTest("test_vm_boot_time", (builder) -> builder);
+ BootTimeStats stats =
+ runBootTimeTest(
+ "test_vm_boot_time",
+ (builder) -> builder.setCpuTopology(CPU_TOPOLOGY_ONE_CPU));
reportMetrics(stats.get(BootTimeMetric.TOTAL), "boot_time", "ms");
}
@Test
- public void testMicrodroidMulticoreBootTime()
+ public void testMicrodroidHostCpuTopologyBootTime()
throws VirtualMachineException, InterruptedException, IOException {
- for (int numCpus : new int[] {2, 4, 8}) {
- BootTimeStats stats =
- runBootTimeTest(
- "test_vm_boot_time_multicore",
- (builder) -> builder.setNumCpus(numCpus));
- String metricName = "boot_time_" + numCpus + "cpus";
- reportMetrics(stats.get(BootTimeMetric.TOTAL), metricName, "ms");
- }
+ BootTimeStats stats =
+ runBootTimeTest(
+ "test_vm_boot_time_host_topology",
+ (builder) -> builder.setCpuTopology(CPU_TOPOLOGY_MATCH_HOST));
+ reportMetrics(stats.get(BootTimeMetric.TOTAL), "boot_time", "ms");
}
@Test
@@ -248,7 +260,7 @@
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config_io.json")
- .setDebugLevel(DEBUG_LEVEL_FULL)
+ .setDebugLevel(DEBUG_LEVEL_NONE)
.build();
List<Double> transferRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
@@ -275,7 +287,7 @@
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadConfigPath("assets/vm_config_io.json")
- .setDebugLevel(DEBUG_LEVEL_FULL)
+ .setDebugLevel(DEBUG_LEVEL_NONE)
.build();
List<Double> readRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
@@ -564,4 +576,115 @@
}
}
}
+
+ @Test
+ public void testRpcBinderLatency() throws Exception {
+ final int NUM_WARMUPS = 10;
+ final int NUM_REQUESTS = 10_000;
+
+ VirtualMachineConfig config =
+ newVmConfigBuilder()
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+ .setDebugLevel(DEBUG_LEVEL_NONE)
+ .build();
+
+ 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) -> {
+ // 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, "latency/rpcbinder", "us");
+ }
+
+ @Test
+ public void testVsockLatency() throws Exception {
+ final int NUM_WARMUPS = 10;
+ final int NUM_REQUESTS = 10_000;
+
+ VirtualMachineConfig config =
+ newVmConfigBuilder()
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+ .setDebugLevel(DEBUG_LEVEL_NONE)
+ .build();
+
+ 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);
+
+ // Correctness check.
+ writer.write("hello\n");
+ writer.flush();
+ tr.mFileContent = reader.readLine().trim();
+
+ // Warmup.
+ for (int j = 0; j < NUM_WARMUPS; ++j) {
+ String text = "test" + j + "\n";
+ writer.write(text);
+ writer.flush();
+ reader.readLine();
+ }
+
+ // 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_hostside/java/android/avf/test/AVFHostTestCase.java b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
index c47e915..9c8714f 100644
--- a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
+++ b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
@@ -79,14 +79,16 @@
private static final int ROUND_IGNORE_STARTUP_TIME = 3;
private static final String APK_NAME = "MicrodroidTestApp.apk";
private static final String PACKAGE_NAME = "com.android.microdroid.test";
- private static final int NUM_VCPUS = 3;
private MetricsProcessor mMetricsProcessor;
@Rule public TestMetrics mMetrics = new TestMetrics();
+ private boolean mNeedTearDown = false;
+
@Before
public void setUp() throws Exception {
testIfDeviceIsCapable(getDevice());
+ mNeedTearDown = true;
getDevice().installPackage(findTestFile(APK_NAME), /* reinstall */ false);
@@ -95,6 +97,12 @@
@After
public void tearDown() throws Exception {
+ if (!mNeedTearDown) {
+ // If we skipped setUp, we don't need to undo it, and that avoids potential exceptions
+ // incompatible hardware. (Note that tests can change what testIfDeviceIsCapable()
+ // sees, so we can't rely on that - b/268688303.)
+ return;
+ }
// Set PKVM enable and reboot to prevent previous staged session.
if (!isCuttlefish()) {
setPKVMStatusWithRebootToBootloader(true);
@@ -247,7 +255,7 @@
MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
.debugLevel("full")
.memoryMib(vm_mem_mb)
- .numCpus(NUM_VCPUS)
+ .cpuTopology("match_host")
.build(device);
microdroidDevice.waitForBootComplete(30000);
microdroidDevice.enableAdbRoot();
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..23f8ca6 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,6 +20,7 @@
/** 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;
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..744f94c 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,23 @@
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 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 +90,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 +447,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 6e0cf5a..78500af 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -2,6 +2,54 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+genrule_defaults {
+ name: "test_avf_debug_policy_overlay",
+ tools: ["dtc"],
+ cmd: "$(location dtc) -I dts -O dtb $(in) -o $(out)",
+}
+
+genrule {
+ name: "test_avf_debug_policy_with_ramdump",
+ defaults: ["test_avf_debug_policy_overlay"],
+ srcs: ["assets/avf_debug_policy_with_ramdump.dts"],
+ out: ["avf_debug_policy_with_ramdump.dtbo"],
+}
+
+genrule {
+ name: "test_avf_debug_policy_without_ramdump",
+ defaults: ["test_avf_debug_policy_overlay"],
+ srcs: ["assets/avf_debug_policy_without_ramdump.dts"],
+ 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"],
+}
+
+genrule {
+ name: "test_avf_debug_policy_with_adb",
+ defaults: ["test_avf_debug_policy_overlay"],
+ srcs: ["assets/avf_debug_policy_with_adb.dts"],
+ out: ["avf_debug_policy_with_adb.dtbo"],
+}
+
+genrule {
+ name: "test_avf_debug_policy_without_adb",
+ defaults: ["test_avf_debug_policy_overlay"],
+ srcs: ["assets/avf_debug_policy_without_adb.dts"],
+ out: ["avf_debug_policy_without_adb.dtbo"],
+}
+
java_test_host {
name: "MicrodroidHostTestCases",
srcs: ["java/**/*.java"],
@@ -10,12 +58,14 @@
"general-tests",
],
libs: [
+ "androidx.annotation_annotation",
"tradefed",
],
static_libs: [
"MicrodroidHostTestHelper",
"compatibility-host-util",
"cts-statsd-atom-host-test-utils",
+ "microdroid_payload_metadata",
],
per_testcase_directory: true,
data: [
@@ -23,6 +73,14 @@
":microdroid_general_sepolicy.conf",
":test.com.android.virt.pem",
":test2.com.android.virt.pem",
+ ":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",
+ ":test_avf_debug_policy_with_adb",
+ ":test_avf_debug_policy_without_adb",
+ "assets/bcc.dat",
],
data_native_bins: [
"sepolicy-analyze",
@@ -32,7 +90,6 @@
"initrd_bootconfig",
"lpmake",
"lpunpack",
- "mk_payload",
"sign_virt_apex",
"simg2img",
],
diff --git a/tests/hostside/assets/avf_debug_policy_with_adb.dts b/tests/hostside/assets/avf_debug_policy_with_adb.dts
new file mode 100644
index 0000000..9ad15dd
--- /dev/null
+++ b/tests/hostside/assets/avf_debug_policy_with_adb.dts
@@ -0,0 +1,18 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+ fragment@avf {
+ target-path = "/";
+
+ __overlay__ {
+ avf {
+ guest {
+ microdroid {
+ adb = <1>;
+ };
+ };
+ };
+ };
+ };
+};
\ No newline at end of file
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_with_ramdump.dts b/tests/hostside/assets/avf_debug_policy_with_ramdump.dts
new file mode 100644
index 0000000..26db7be
--- /dev/null
+++ b/tests/hostside/assets/avf_debug_policy_with_ramdump.dts
@@ -0,0 +1,21 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+ fragment@avf {
+ target-path = "/";
+
+ __overlay__ {
+ avf {
+ guest {
+ common {
+ ramdump = <1>;
+ };
+ microdroid {
+ adb = <1>; // adb is required to check VM's bootargs.
+ };
+ };
+ };
+ };
+ };
+};
\ No newline at end of file
diff --git a/tests/hostside/assets/avf_debug_policy_without_adb.dts b/tests/hostside/assets/avf_debug_policy_without_adb.dts
new file mode 100644
index 0000000..992e0ff
--- /dev/null
+++ b/tests/hostside/assets/avf_debug_policy_without_adb.dts
@@ -0,0 +1,18 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+ fragment@avf {
+ target-path = "/";
+
+ __overlay__ {
+ avf {
+ guest {
+ microdroid {
+ adb = <0>;
+ };
+ };
+ };
+ };
+ };
+};
\ 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/assets/avf_debug_policy_without_ramdump.dts b/tests/hostside/assets/avf_debug_policy_without_ramdump.dts
new file mode 100644
index 0000000..194e314
--- /dev/null
+++ b/tests/hostside/assets/avf_debug_policy_without_ramdump.dts
@@ -0,0 +1,21 @@
+/dts-v1/;
+/plugin/;
+
+/ {
+ fragment@avf {
+ target-path = "/";
+
+ __overlay__ {
+ avf {
+ guest {
+ common {
+ ramdump = <0>;
+ };
+ microdroid {
+ adb = <1>; // adb is required to check VM's bootargs.
+ };
+ };
+ };
+ };
+ };
+};
\ No newline at end of file
diff --git a/tests/hostside/assets/bcc.dat b/tests/hostside/assets/bcc.dat
new file mode 100644
index 0000000..7ab71f1
--- /dev/null
+++ b/tests/hostside/assets/bcc.dat
Binary files differ
diff --git a/tests/hostside/helper/Android.bp b/tests/hostside/helper/Android.bp
index 6196ec5..e8b6f36 100644
--- a/tests/hostside/helper/Android.bp
+++ b/tests/hostside/helper/Android.bp
@@ -6,6 +6,7 @@
name: "MicrodroidHostTestHelper",
srcs: ["java/**/*.java"],
libs: [
+ "androidx.annotation_annotation",
"compatibility-tradefed",
"tradefed",
"truth-prebuilt",
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java b/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java
new file mode 100644
index 0000000..95eaa58
--- /dev/null
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java
@@ -0,0 +1,143 @@
+/*
+ * 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.test.host;
+
+import static java.nio.ByteOrder.LITTLE_ENDIAN;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+import java.nio.ByteBuffer;
+
+/** pvmfw.bin with custom config payloads on host. */
+public final class Pvmfw {
+ private static final int SIZE_8B = 8; // 8 bytes
+ private static final int SIZE_4K = 4 << 10; // 4 KiB, PAGE_SIZE
+ private static final int BUFFER_SIZE = 1024;
+ private static final int HEADER_SIZE = Integer.BYTES * 8; // Header has 8 integers.
+ private static final int HEADER_MAGIC = 0x666d7670;
+ private static final int HEADER_VERSION = getVersion(1, 0);
+ private static final int HEADER_FLAGS = 0;
+
+ @NonNull private final File mPvmfwBinFile;
+ @NonNull private final File mBccFile;
+ @Nullable private final File mDebugPolicyFile;
+
+ private Pvmfw(
+ @NonNull File pvmfwBinFile, @NonNull File bccFile, @Nullable File debugPolicyFile) {
+ mPvmfwBinFile = Objects.requireNonNull(pvmfwBinFile);
+ mBccFile = Objects.requireNonNull(bccFile);
+ mDebugPolicyFile = debugPolicyFile;
+ }
+
+ /**
+ * Serializes pvmfw.bin and its config, as written in the <a
+ * href="https://android.googlesource.com/platform/packages/modules/Virtualization/+/master/pvmfw/README.md">README.md</a>
+ */
+ public void serialize(@NonNull File outFile) throws IOException {
+ Objects.requireNonNull(outFile);
+
+ int bccOffset = HEADER_SIZE;
+ int bccSize = (int) mBccFile.length();
+
+ int debugPolicyOffset = alignTo(bccOffset + bccSize, SIZE_8B);
+ int debugPolicySize = mDebugPolicyFile == null ? 0 : (int) mDebugPolicyFile.length();
+
+ int totalSize = debugPolicyOffset + debugPolicySize;
+
+ ByteBuffer header = ByteBuffer.allocate(HEADER_SIZE).order(LITTLE_ENDIAN);
+ header.putInt(HEADER_MAGIC);
+ header.putInt(HEADER_VERSION);
+ header.putInt(totalSize);
+ header.putInt(HEADER_FLAGS);
+ header.putInt(bccOffset);
+ header.putInt(bccSize);
+ header.putInt(debugPolicyOffset);
+ header.putInt(debugPolicySize);
+
+ try (FileOutputStream pvmfw = new FileOutputStream(outFile)) {
+ appendFile(pvmfw, mPvmfwBinFile);
+ padTo(pvmfw, SIZE_4K);
+ pvmfw.write(header.array());
+ padTo(pvmfw, HEADER_SIZE);
+ appendFile(pvmfw, mBccFile);
+ if (mDebugPolicyFile != null) {
+ padTo(pvmfw, SIZE_8B);
+ appendFile(pvmfw, mDebugPolicyFile);
+ }
+ padTo(pvmfw, SIZE_4K);
+ }
+ }
+
+ private void appendFile(@NonNull FileOutputStream out, @NonNull File inFile)
+ throws IOException {
+ byte buffer[] = new byte[BUFFER_SIZE];
+ try (FileInputStream in = new FileInputStream(inFile)) {
+ int size;
+ while (true) {
+ size = in.read(buffer);
+ if (size < 0) {
+ return;
+ }
+ out.write(buffer, /* offset= */ 0, size);
+ }
+ }
+ }
+
+ private void padTo(@NonNull FileOutputStream out, int size) throws IOException {
+ int streamSize = (int) out.getChannel().size();
+ for (int i = streamSize; i < alignTo(streamSize, size); i++) {
+ out.write(0); // write byte.
+ }
+ }
+
+ private static int alignTo(int x, int size) {
+ return (x + size - 1) & ~(size - 1);
+ }
+
+ private static int getVersion(int major, int minor) {
+ return ((major & 0xFFFF) << 16) | (minor & 0xFFFF);
+ }
+
+ /** Builder for {@link Pvmfw}. */
+ public static final class Builder {
+ @NonNull private final File mPvmfwBinFile;
+ @NonNull private final File mBccFile;
+ @Nullable private File mDebugPolicyFile;
+
+ public Builder(@NonNull File pvmfwBinFile, @NonNull File bccFile) {
+ mPvmfwBinFile = Objects.requireNonNull(pvmfwBinFile);
+ mBccFile = Objects.requireNonNull(bccFile);
+ }
+
+ @NonNull
+ public Builder setDebugPolicyOverlay(@Nullable File debugPolicyFile) {
+ mDebugPolicyFile = debugPolicyFile;
+ return this;
+ }
+
+ @NonNull
+ public Pvmfw build() {
+ return new Pvmfw(mPvmfwBinFile, mBccFile, mDebugPolicyFile);
+ }
+ }
+}
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index d40dcba..687756e 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -50,12 +50,12 @@
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.xml.AbstractXmlParser;
+import com.android.virt.PayloadMetadata;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
@@ -73,6 +73,7 @@
import java.io.PipedOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -92,9 +93,6 @@
private static final int MIN_MEM_ARM64 = 145;
private static final int MIN_MEM_X86_64 = 196;
- // Number of vCPUs for testing purpose
- private static final int NUM_VCPUS = 3;
-
private static final int BOOT_COMPLETE_TIMEOUT = 30000; // 30 seconds
private static final Pattern sCIDPattern = Pattern.compile("with CID (\\d+)");
@@ -135,42 +133,14 @@
private void createPayloadMetadata(List<ActiveApexInfo> apexes, File payloadMetadata)
throws Exception {
- // mk_payload's config
- File configFile = new File(payloadMetadata.getParentFile(), "payload_config.json");
- JSONObject config = new JSONObject();
- config.put(
- "apk",
- new JSONObject(Map.of("name", "microdroid-apk", "path", "", "idsig_path", "")));
- config.put("payload_config_path", "/mnt/apk/assets/vm_config.json");
- config.put(
- "apexes",
- new JSONArray(
+ PayloadMetadata.write(
+ PayloadMetadata.metadata(
+ "/mnt/apk/assets/vm_config.json",
+ PayloadMetadata.apk("microdroid-apk"),
apexes.stream()
- .map(apex -> new JSONObject(Map.of("name", apex.name, "path", "")))
- .collect(toList())));
- FileUtil.writeToFile(config.toString(), configFile);
-
- RunUtil runUtil = new RunUtil();
- String command =
- String.join(
- " ",
- findTestFile("mk_payload").getAbsolutePath(),
- "--metadata-only",
- configFile.getAbsolutePath(),
- payloadMetadata.getAbsolutePath());
- // mk_payload should run fast enough
- CommandResult result = runUtil.runTimedCmd(5000, "/bin/bash", "-c", command);
- String out = result.getStdout();
- String err = result.getStderr();
- assertWithMessage(
- "creating payload metadata failed:\n\tout: "
- + out
- + "\n\terr: "
- + err
- + "\n")
- .about(command_results())
- .that(result)
- .isSuccess();
+ .map(apex -> PayloadMetadata.apex(apex.name))
+ .collect(toList())),
+ payloadMetadata);
}
private void resignVirtApex(
@@ -222,6 +192,14 @@
assertThat(callable.call(), matcher);
}
+ private int getDeviceNumCpus(CommandRunner runner) throws DeviceNotAvailableException {
+ return Integer.parseInt(runner.run("nproc --all").trim());
+ }
+
+ private int getDeviceNumCpus(ITestDevice device) throws DeviceNotAvailableException {
+ return getDeviceNumCpus(new CommandRunner(device));
+ }
+
static class ActiveApexInfo {
public String name;
public String path;
@@ -443,7 +421,7 @@
MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
.debugLevel("full")
.memoryMib(minMemorySize())
- .numCpus(NUM_VCPUS)
+ .cpuTopology("match_host")
.protectedVm(protectedVm)
.build(getAndroidDevice());
@@ -548,13 +526,14 @@
return !result.trim().isEmpty();
}
- private boolean isTombstoneGeneratedWithCmd(String configPath, String... crashCommand)
- throws Exception {
+ private boolean isTombstoneGeneratedWithCmd(
+ boolean protectedVm, String configPath, String... crashCommand) throws Exception {
mMicrodroidDevice =
MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
.debugLevel("full")
.memoryMib(minMemorySize())
- .numCpus(NUM_VCPUS)
+ .cpuTopology("match_host")
+ .protectedVm(protectedVm)
.build(getAndroidDevice());
mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
mMicrodroidDevice.enableAdbRoot();
@@ -573,6 +552,7 @@
public void testTombstonesAreGeneratedUponUserspaceCrash() throws Exception {
assertThat(
isTombstoneGeneratedWithCmd(
+ false,
"assets/vm_config.json",
"kill",
"-SIGSEGV",
@@ -584,6 +564,7 @@
public void testTombstonesAreNotGeneratedIfNotExportedUponUserspaceCrash() throws Exception {
assertThat(
isTombstoneGeneratedWithCmd(
+ false,
"assets/vm_config_no_tombstone.json",
"kill",
"-SIGSEGV",
@@ -591,18 +572,35 @@
.isFalse();
}
- @Test
- @Ignore("b/243630590: Temporal workaround until lab devices has flashed new DPM")
- public void testTombstonesAreGeneratedUponKernelCrash() throws Exception {
+ private void testTombstonesAreGeneratedUponKernelCrash(boolean protectedVm) throws Exception {
assumeFalse("Cuttlefish is not supported", isCuttlefish());
assumeFalse("Skipping test because ramdump is disabled on user build", isUserBuild());
assertThat(
isTombstoneGeneratedWithCmd(
- "assets/vm_config.json", "echo", "c", ">", "/proc/sysrq-trigger"))
+ protectedVm,
+ "assets/vm_config.json",
+ "echo",
+ "c",
+ ">",
+ "/proc/sysrq-trigger"))
.isTrue();
}
- private boolean isTombstoneGeneratedWithCrashPayload(boolean debuggable) throws Exception {
+ @Test
+ public void testTombstonesAreGeneratedUponKernelCrashOnNonPvm() throws Exception {
+ testTombstonesAreGeneratedUponKernelCrash(false);
+ }
+
+ @Test
+ public void testTombstonesAreGeneratedUponKernelCrashOnPvm() throws Exception {
+ assumeTrue(
+ "Protected VMs are not supported",
+ getAndroidDevice().supportsMicrodroid(/*protectedVm=*/ true));
+ testTombstonesAreGeneratedUponKernelCrash(true);
+ }
+
+ private boolean isTombstoneGeneratedWithVmRunApp(boolean debuggable, String... additionalArgs)
+ throws Exception {
// we can't use microdroid builder as it wants ADB connection (debuggable)
CommandRunner android = new CommandRunner(getDevice());
@@ -612,20 +610,27 @@
final String apkPath = getPathForPackage(PACKAGE_NAME);
final String idsigPath = TEST_ROOT + "idsig";
final String instanceImgPath = TEST_ROOT + "instance.img";
- android.run(
- VIRT_APEX + "bin/vm",
- "run-app",
- "--payload-binary-name",
- "MicrodroidCrashNativeLib.so",
- "--debug",
- debuggable ? "full" : "none",
- apkPath,
- idsigPath,
- instanceImgPath);
+ List<String> cmd =
+ new ArrayList<>(
+ Arrays.asList(
+ VIRT_APEX + "bin/vm",
+ "run-app",
+ "--debug",
+ debuggable ? "full" : "none",
+ apkPath,
+ idsigPath,
+ instanceImgPath));
+ Collections.addAll(cmd, additionalArgs);
+ android.run(cmd.toArray(new String[0]));
return isTombstoneReceivedFromHostLogcat();
}
+ private boolean isTombstoneGeneratedWithCrashPayload(boolean debuggable) throws Exception {
+ return isTombstoneGeneratedWithVmRunApp(
+ debuggable, "--payload-binary-name", "MicrodroidCrashNativeLib.so");
+ }
+
@Test
public void testTombstonesAreGeneratedWithCrashPayload() throws Exception {
assertThat(isTombstoneGeneratedWithCrashPayload(true /* debuggable */)).isTrue();
@@ -636,6 +641,21 @@
assertThat(isTombstoneGeneratedWithCrashPayload(false /* debuggable */)).isFalse();
}
+ private boolean isTombstoneGeneratedWithCrashConfig(boolean debuggable) throws Exception {
+ return isTombstoneGeneratedWithVmRunApp(
+ debuggable, "--config-path", "assets/vm_config_crash.json");
+ }
+
+ @Test
+ public void testTombstonesAreGeneratedWithCrashConfig() throws Exception {
+ assertThat(isTombstoneGeneratedWithCrashConfig(true /* debuggable */)).isTrue();
+ }
+
+ @Test
+ public void testTombstonesAreNotGeneratedWithCrashConfigWhenNonDebuggable() throws Exception {
+ assertThat(isTombstoneGeneratedWithCrashConfig(false /* debuggable */)).isFalse();
+ }
+
@Test
public void testTelemetryPushedAtoms() throws Exception {
// Reset statsd config and report before the test
@@ -657,7 +677,7 @@
MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
.debugLevel("full")
.memoryMib(minMemorySize())
- .numCpus(NUM_VCPUS)
+ .cpuTopology("match_host")
.build(device);
microdroid.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
device.shutdownMicrodroid(microdroid);
@@ -685,7 +705,7 @@
assertThat(atomVmCreationRequested.getVmIdentifier()).isEqualTo("VmRunApp");
assertThat(atomVmCreationRequested.getConfigType())
.isEqualTo(AtomsProto.VmCreationRequested.ConfigType.VIRTUAL_MACHINE_APP_CONFIG);
- assertThat(atomVmCreationRequested.getNumCpus()).isEqualTo(NUM_VCPUS);
+ assertThat(atomVmCreationRequested.getNumCpus()).isEqualTo(getDeviceNumCpus(device));
assertThat(atomVmCreationRequested.getMemoryMib()).isEqualTo(minMemorySize());
assertThat(atomVmCreationRequested.getApexes())
.isEqualTo("com.android.art:com.android.compos:com.android.sdkext");
@@ -720,7 +740,7 @@
MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
.debugLevel("full")
.memoryMib(minMemorySize())
- .numCpus(NUM_VCPUS)
+ .cpuTopology("match_host")
.build(getAndroidDevice());
mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
@@ -749,8 +769,7 @@
assertThat(android.tryRun("egrep", "'avc:[[:space:]]{1,2}denied'", LOG_PATH)).isNull();
assertThat(android.tryRun("egrep", "'avc:[[:space:]]{1,2}denied'", CONSOLE_PATH)).isNull();
- assertThat(microdroid.run("cat /proc/cpuinfo | grep processor | wc -l"))
- .isEqualTo(Integer.toString(NUM_VCPUS));
+ assertThat(getDeviceNumCpus(microdroid)).isEqualTo(getDeviceNumCpus(android));
// Check that selinux is enabled
assertThat(microdroid.run("getenforce")).isEqualTo("Enforcing");
@@ -786,7 +805,7 @@
MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
.debugLevel("full")
.memoryMib(minMemorySize())
- .numCpus(NUM_VCPUS)
+ .cpuTopology("match_host")
.build(getAndroidDevice());
mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
mMicrodroidDevice.enableAdbRoot();
@@ -826,6 +845,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
new file mode 100644
index 0000000..10f7003
--- /dev/null
+++ b/tests/hostside/java/com/android/microdroid/test/PvmfwDebugPolicyHostTests.java
@@ -0,0 +1,300 @@
+/*
+ * 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.test;
+
+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;
+
+import com.android.microdroid.test.host.CommandRunner;
+import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
+import com.android.microdroid.test.host.Pvmfw;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.TestDevice;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+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 */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class PvmfwDebugPolicyHostTests extends MicrodroidHostTestCaseBase {
+ @NonNull private static final String PVMFW_FILE_NAME = "pvmfw_test.bin";
+ @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_FULL = "full";
+ @NonNull private static final String MICRODROID_DEBUG_NONE = "none";
+ @NonNull private static final String MICRODROID_CONFIG_PATH = "assets/vm_config_apex.json";
+ @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 BOOT_FAILURE_WAIT_TIME_MS = 10000; // 10 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_PROP = "hypervisor.pvmfw.path";
+
+ @NonNull private static final String MICRODROID_CMDLINE_PATH = "/proc/cmdline";
+ @NonNull private static final String MICRODROID_DT_ROOT_PATH = "/proc/device-tree";
+
+ @NonNull
+ private static final String MICRODROID_DT_BOOTARGS_PATH =
+ MICRODROID_DT_ROOT_PATH + "/chosen/bootargs";
+
+ @NonNull
+ private static final String MICRODROID_DT_RAMDUMP_PATH =
+ MICRODROID_DT_ROOT_PATH + "/avf/guest/common/ramdump";
+
+ @NonNull private static final String HEX_STRING_ZERO = "00000000";
+ @NonNull private static final String HEX_STRING_ONE = "00000001";
+
+ @Nullable private static File mPvmfwBinFileOnHost;
+ @Nullable private static File mBccFileOnHost;
+
+ @Nullable private TestDevice mAndroidDevice;
+ @Nullable private ITestDevice mMicrodroidDevice;
+ @Nullable private File mCustomPvmfwBinFileOnHost;
+
+ @Before
+ public void setUp() throws Exception {
+ mAndroidDevice = (TestDevice) Objects.requireNonNull(getDevice());
+ 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();
+
+ // tradefed copies the test artfacts under /tmp when running tests,
+ // so we should *find* the artifacts with the file name.
+ mPvmfwBinFileOnHost =
+ getTestInformation().getDependencyFile(PVMFW_FILE_NAME, /* targetFirst= */ false);
+ mBccFileOnHost =
+ getTestInformation().getDependencyFile(BCC_FILE_NAME, /* targetFirst= */ false);
+
+ // Check device capability
+ testIfDeviceIsCapable(mAndroidDevice);
+ assumeTrue(
+ "Protected VMs are not supported",
+ mAndroidDevice.supportsMicrodroid(/*protectedVm=*/ true));
+
+ // Prepare for loading pvmfw.bin
+ // File will be setup in individual test,
+ // and then pushed to device in launchProtectedVmAndWaitForBootCompleted.
+ mCustomPvmfwBinFileOnHost =
+ FileUtil.createTempFile(CUSTOM_PVMFW_FILE_PREFIX, CUSTOM_PVMFW_FILE_SUFFIX);
+ mAndroidDevice.setProperty(CUSTOM_PVMFW_IMG_PATH_PROP, CUSTOM_PVMFW_IMG_PATH);
+
+ // Prepare for launching microdroid
+ mAndroidDevice.installPackage(findTestFile(PACKAGE_FILE_NAME), /* reinstall */ false);
+ prepareVirtualizationTestSetup(mAndroidDevice);
+ mMicrodroidDevice = null;
+ }
+
+ @After
+ public void shutdown() throws Exception {
+ if (!mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true)) {
+ return;
+ }
+ if (mMicrodroidDevice != null) {
+ mAndroidDevice.shutdownMicrodroid(mMicrodroidDevice);
+ mMicrodroidDevice = null;
+ }
+ mAndroidDevice.uninstallPackage(PACKAGE_NAME);
+
+ // Cleanup for custom pvmfw.bin
+ mAndroidDevice.setProperty(CUSTOM_PVMFW_IMG_PATH_PROP, "");
+ FileUtil.deleteFile(mCustomPvmfwBinFileOnHost);
+
+ cleanUpVirtualizationTestSetup(mAndroidDevice);
+
+ mAndroidDevice.disableAdbRoot();
+ }
+
+ @Test
+ public void testRamdump() throws Exception {
+ Pvmfw pvmfw = createPvmfw("avf_debug_policy_with_ramdump.dtbo");
+ pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+ mMicrodroidDevice = launchProtectedVmAndWaitForBootCompleted(MICRODROID_DEBUG_FULL);
+
+ assertThat(readMicrodroidFileAsString(MICRODROID_CMDLINE_PATH)).contains("crashkernel=");
+ assertThat(readMicrodroidFileAsString(MICRODROID_DT_BOOTARGS_PATH))
+ .contains("crashkernel=");
+ assertThat(readMicrodroidFileAsHexString(MICRODROID_DT_RAMDUMP_PATH))
+ .isEqualTo(HEX_STRING_ONE);
+ }
+
+ @Test
+ public void testNoRamdump() throws Exception {
+ Pvmfw pvmfw = createPvmfw("avf_debug_policy_without_ramdump.dtbo");
+ pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+ mMicrodroidDevice = launchProtectedVmAndWaitForBootCompleted(MICRODROID_DEBUG_FULL);
+
+ assertThat(readMicrodroidFileAsString(MICRODROID_CMDLINE_PATH))
+ .doesNotContain("crashkernel=");
+ assertThat(readMicrodroidFileAsString(MICRODROID_DT_BOOTARGS_PATH))
+ .doesNotContain("crashkernel=");
+ assertThat(readMicrodroidFileAsHexString(MICRODROID_DT_RAMDUMP_PATH))
+ .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();
+ }
+
+ @Test
+ public void testNoAdb_boots() throws Exception {
+ Pvmfw pvmfw = createPvmfw("avf_debug_policy_without_adb.dtbo");
+ pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+ // VM would boot, but cannot verify directly because of no adbd in the VM.
+ CommandResult result = tryLaunchProtectedNonDebuggableVm();
+ assertThat(result.getStatus()).isEqualTo(CommandStatus.TIMED_OUT);
+ assertWithMessage("Microdroid should have booted")
+ .that(result.getStderr())
+ .contains("payload is ready");
+ }
+
+ @Test
+ public void testNoAdb_noConnection() throws Exception {
+ Pvmfw pvmfw = createPvmfw("avf_debug_policy_without_adb.dtbo");
+ pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+ try {
+ launchProtectedVmAndWaitForBootCompleted(
+ MICRODROID_DEBUG_NONE, BOOT_FAILURE_WAIT_TIME_MS);
+ assertWithMessage("adb shouldn't be available").fail();
+ } catch (Exception e) {
+ // expected exception. passthrough.
+ }
+ }
+
+ @NonNull
+ private String readMicrodroidFileAsString(@NonNull String path)
+ throws DeviceNotAvailableException {
+ return new CommandRunner(mMicrodroidDevice).run("cat", path);
+ }
+
+ @NonNull
+ private String readMicrodroidFileAsHexString(@NonNull String path)
+ throws DeviceNotAvailableException {
+ return new CommandRunner(mMicrodroidDevice).run("xxd", "-p", path);
+ }
+
+ @NonNull
+ private Pvmfw createPvmfw(@NonNull String debugPolicyFileName) throws FileNotFoundException {
+ File file =
+ getTestInformation()
+ .getDependencyFile(debugPolicyFileName, /* targetFirst= */ false);
+ return new Pvmfw.Builder(mPvmfwBinFileOnHost, mBccFileOnHost)
+ .setDebugPolicyOverlay(file)
+ .build();
+ }
+
+ @NonNull
+ private boolean hasConsoleOutput(CommandResult result) throws DeviceNotAvailableException {
+ return result.getStdout().contains("Run /init as init process");
+ }
+
+ private ITestDevice launchProtectedVmAndWaitForBootCompleted(String debugLevel)
+ throws DeviceNotAvailableException {
+ return launchProtectedVmAndWaitForBootCompleted(debugLevel, BOOT_COMPLETE_TIMEOUT_MS);
+ }
+
+ private ITestDevice launchProtectedVmAndWaitForBootCompleted(
+ String debugLevel, long adbTimeoutMs) throws DeviceNotAvailableException {
+ mMicrodroidDevice =
+ MicrodroidBuilder.fromDevicePath(
+ getPathForPackage(PACKAGE_NAME), MICRODROID_CONFIG_PATH)
+ .debugLevel(debugLevel)
+ .protectedVm(/* protectedVm= */ true)
+ .addBootFile(mCustomPvmfwBinFileOnHost, PVMFW_FILE_NAME)
+ .setAdbConnectTimeoutMs(adbTimeoutMs)
+ .build(mAndroidDevice);
+ 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/hostside/tools/Android.bp b/tests/hostside/tools/Android.bp
new file mode 100644
index 0000000..f3cc275
--- /dev/null
+++ b/tests/hostside/tools/Android.bp
@@ -0,0 +1,10 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_binary_host {
+ name: "pvmfw-tool",
+ manifest: "pvmfw-tool-manifest.txt",
+ srcs: ["PvmfwTool.java"],
+ static_libs: ["MicrodroidHostTestHelper"],
+}
diff --git a/tests/hostside/tools/PvmfwTool.java b/tests/hostside/tools/PvmfwTool.java
new file mode 100644
index 0000000..18dd6d7
--- /dev/null
+++ b/tests/hostside/tools/PvmfwTool.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+import com.android.microdroid.test.host.Pvmfw;
+
+import java.io.File;
+import java.io.IOException;
+
+/** CLI for {@link com.android.microdroid.test.host.Pvmfw}. */
+public class PvmfwTool {
+ public static void printUsage() {
+ System.out.println("pvmfw-tool: Appends pvmfw.bin and config payloads.");
+ System.out.println("Requires BCC and debug policy dtbo files");
+ System.out.println("");
+ System.out.println("Usage: pvmfw-tool <pvmfw_with_config> <pvmfw_bin> <bcc.dat> <dp.dtbo>");
+ }
+
+ public static void main(String[] args) {
+ if (args.length != 4) {
+ printUsage();
+ System.exit(1);
+ }
+
+ File out = new File(args[0]);
+ File pvmfw_bin = new File(args[1]);
+ File bcc_dat = new File(args[2]);
+ File dtbo = new File(args[3]);
+
+ try {
+ Pvmfw pvmfw = new Pvmfw.Builder(pvmfw_bin, bcc_dat).setDebugPolicyOverlay(dtbo).build();
+ pvmfw.serialize(out);
+ } catch (IOException e) {
+ e.printStackTrace();
+ printUsage();
+ System.exit(1);
+ }
+ }
+}
diff --git a/tests/hostside/tools/pvmfw-tool-manifest.txt b/tests/hostside/tools/pvmfw-tool-manifest.txt
new file mode 100644
index 0000000..dc71fd2
--- /dev/null
+++ b/tests/hostside/tools/pvmfw-tool-manifest.txt
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: com.android.microdroid.PvmfwTool
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 3c487ee..fe8f5c9 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -8,6 +8,10 @@
"cts",
"general-tests",
],
+ static_libs: [
+ "com.android.microdroid.testservice-java",
+ "com.android.microdroid.test.vmshare_service-java",
+ ],
sdk_version: "test_current",
jni_uses_platform_apis: true,
use_embedded_native_libs: true,
@@ -25,9 +29,9 @@
"androidx.test.ext.junit",
"authfs_test_apk_assets",
"cbor-java",
- "com.android.microdroid.testservice-java",
"truth-prebuilt",
"compatibility-common-util-devicesidelib",
+ "measure_io_as_jar",
],
jni_libs: [
"MicrodroidTestNativeLib",
@@ -59,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/assets/vm_config_crash.json b/tests/testapk/assets/vm_config_crash.json
new file mode 100644
index 0000000..ce6af80
--- /dev/null
+++ b/tests/testapk/assets/vm_config_crash.json
@@ -0,0 +1,9 @@
+{
+ "os": {
+ "name": "microdroid"
+ },
+ "task": {
+ "type": "microdroid_launcher",
+ "command": "MicrodroidCrashNativeLib.so"
+ }
+}
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 e0748c1..542f595 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -18,25 +18,35 @@
import static android.system.virtualmachine.VirtualMachine.STATUS_DELETED;
import static android.system.virtualmachine.VirtualMachine.STATUS_RUNNING;
import static android.system.virtualmachine.VirtualMachine.STATUS_STOPPED;
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_NON_PROTECTED_VM;
import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_PROTECTED_VM;
-
import static com.google.common.truth.Truth.assertThat;
+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 static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import com.google.common.base.Strings;
+import com.google.common.truth.BooleanSubject;
+import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.ServiceConnection;
import android.os.Build;
+import android.os.IBinder;
+import android.os.Parcel;
import android.os.ParcelFileDescriptor;
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;
@@ -45,14 +55,12 @@
import android.system.virtualmachine.VirtualMachineManager;
import android.util.Log;
-import androidx.test.core.app.ApplicationProvider;
-
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.google.common.base.Strings;
-import com.google.common.truth.BooleanSubject;
+import com.android.microdroid.testservice.IVmCallback;
import org.junit.After;
import org.junit.Before;
@@ -85,6 +93,8 @@
import java.util.OptionalLong;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import co.nstant.in.cbor.CborDecoder;
@@ -127,21 +137,21 @@
private static final String VM_SHARE_APP_PACKAGE_NAME = "com.android.microdroid.vmshare_app";
- @Test
- @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
- public void createAndConnectToVm() throws Exception {
- assumeSupportedKernel();
+ private void createAndConnectToVmHelper(int cpuTopology) throws Exception {
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
.setDebugLevel(DEBUG_LEVEL_FULL)
+ .setCpuTopology(cpuTopology)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mAddInteger = ts.addInteger(123, 456);
@@ -150,7 +160,7 @@
tr.mApkContentsPath = ts.getApkContentsPath();
tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
});
- assertThat(testResults.mException).isNull();
+ testResults.assertNoException();
assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
assertThat(testResults.mAppRunProp).isEqualTo("true");
assertThat(testResults.mSublibRunProp).isEqualTo("true");
@@ -160,8 +170,20 @@
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+ public void createAndConnectToVm() throws Exception {
+ createAndConnectToVmHelper(CPU_TOPOLOGY_ONE_CPU);
+ }
+
+ @Test
+ @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+ public void createAndConnectToVm_HostCpuTopology() throws Exception {
+ createAndConnectToVmHelper(CPU_TOPOLOGY_MATCH_HOST);
+ }
+
+ @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.
@@ -175,12 +197,8 @@
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
TestResults testResults =
- runVmTestService(
- vm,
- (ts, tr) -> {
- tr.mAddInteger = ts.addInteger(37, 73);
- });
- assertThat(testResults.mException).isNull();
+ runVmTestService(TAG, vm, (ts, tr) -> tr.mAddInteger = ts.addInteger(37, 73));
+ testResults.assertNoException();
assertThat(testResults.mAddInteger).isEqualTo(37 + 73);
}
@@ -192,7 +210,7 @@
"9.17/C-1-4",
})
public void createVmRequiresPermission() {
- assumeSupportedKernel();
+ assumeSupportedDevice();
revokePermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
@@ -211,9 +229,9 @@
}
@Test
- @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+ @CddTest(requirements = {"9.17/C-1-1"})
public void autoCloseVm() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -242,9 +260,63 @@
}
@Test
- @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+ @CddTest(requirements = {"9.17/C-1-1"})
+ public void autoCloseVmDescriptor() throws Exception {
+ VirtualMachineConfig config =
+ newVmConfigBuilder()
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .build();
+ VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
+ VirtualMachineDescriptor descriptor = vm.toDescriptor();
+
+ Parcel parcel = Parcel.obtain();
+ try (descriptor) {
+ // It should be ok to use at this point
+ descriptor.writeToParcel(parcel, 0);
+ }
+
+ // But not now - it's been closed.
+ assertThrows(IllegalStateException.class, () -> descriptor.writeToParcel(parcel, 0));
+ assertThrows(
+ IllegalStateException.class,
+ () -> getVirtualMachineManager().importFromDescriptor("imported_vm", descriptor));
+
+ // Closing again is fine.
+ descriptor.close();
+
+ // Tidy up
+ parcel.recycle();
+ }
+
+ @Test
+ @CddTest(requirements = {"9.17/C-1-1"})
+ public void vmDescriptorClosedOnImport() throws Exception {
+ VirtualMachineConfig config =
+ newVmConfigBuilder()
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .build();
+ VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
+ VirtualMachineDescriptor descriptor = vm.toDescriptor();
+
+ getVirtualMachineManager().importFromDescriptor("imported_vm", descriptor);
+ try {
+ // Descriptor has been implicitly closed
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ getVirtualMachineManager()
+ .importFromDescriptor("imported_vm2", descriptor));
+ } finally {
+ getVirtualMachineManager().delete("imported_vm");
+ }
+ }
+
+ @Test
+ @CddTest(requirements = {"9.17/C-1-1"})
public void vmLifecycleChecks() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -293,7 +365,7 @@
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void connectVsock() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -303,19 +375,15 @@
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_vsock", config);
- AtomicReference<Exception> exception = new AtomicReference<>();
AtomicReference<String> response = new AtomicReference<>();
String request = "Look not into the abyss";
- VmEventListener listener =
- new VmEventListener() {
- @Override
- public void onPayloadReady(VirtualMachine vm) {
- try (vm) {
- ITestService testService =
- ITestService.Stub.asInterface(
- vm.connectToVsockServer(ITestService.SERVICE_PORT));
- testService.runEchoReverseServer();
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (service, results) -> {
+ service.runEchoReverseServer();
ParcelFileDescriptor pfd =
vm.connectVsock(ITestService.ECHO_REVERSE_PORT);
@@ -328,16 +396,62 @@
writer.flush();
response.set(reader.readLine());
}
- } catch (Exception e) {
- exception.set(e);
- }
+ });
+ testResults.assertNoException();
+ assertThat(response.get()).isEqualTo(new StringBuilder(request).reverse().toString());
+ }
+
+ @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);
}
};
- listener.runToFinish(TAG, vm);
- if (exception.get() != null) {
- throw new RuntimeException(exception.get());
- }
- assertThat(response.get()).isEqualTo(new StringBuilder(request).reverse().toString());
+
+ 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
@@ -350,7 +464,7 @@
assertThat(minimal.getApkPath()).isNull();
assertThat(minimal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_NONE);
assertThat(minimal.getMemoryBytes()).isEqualTo(0);
- assertThat(minimal.getNumCpus()).isEqualTo(1);
+ assertThat(minimal.getCpuTopology()).isEqualTo(CPU_TOPOLOGY_ONE_CPU);
assertThat(minimal.getPayloadBinaryName()).isEqualTo("binary.so");
assertThat(minimal.getPayloadConfigPath()).isNull();
assertThat(minimal.isProtectedVm()).isEqualTo(isProtectedVm());
@@ -360,15 +474,14 @@
// Maximal has everything that can be set to some non-default value. (And has different
// values than minimal for the required fields.)
- int maxCpus = Runtime.getRuntime().availableProcessors();
VirtualMachineConfig.Builder maximalBuilder =
new VirtualMachineConfig.Builder(getContext())
.setProtectedVm(mProtectedVm)
.setPayloadConfigPath("config/path")
.setApkPath("/apk/path")
- .setNumCpus(maxCpus)
.setDebugLevel(DEBUG_LEVEL_FULL)
.setMemoryBytes(42)
+ .setCpuTopology(CPU_TOPOLOGY_MATCH_HOST)
.setEncryptedStorageBytes(1_000_000)
.setVmOutputCaptured(true);
VirtualMachineConfig maximal = maximalBuilder.build();
@@ -376,7 +489,7 @@
assertThat(maximal.getApkPath()).isEqualTo("/apk/path");
assertThat(maximal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_FULL);
assertThat(maximal.getMemoryBytes()).isEqualTo(42);
- assertThat(maximal.getNumCpus()).isEqualTo(maxCpus);
+ assertThat(maximal.getCpuTopology()).isEqualTo(CPU_TOPOLOGY_MATCH_HOST);
assertThat(maximal.getPayloadBinaryName()).isNull();
assertThat(maximal.getPayloadConfigPath()).isEqualTo("config/path");
assertThat(maximal.isProtectedVm()).isEqualTo(isProtectedVm());
@@ -408,7 +521,7 @@
IllegalArgumentException.class, () -> builder.setPayloadBinaryName("dir/file.so"));
assertThrows(IllegalArgumentException.class, () -> builder.setDebugLevel(-1));
assertThrows(IllegalArgumentException.class, () -> builder.setMemoryBytes(0));
- assertThrows(IllegalArgumentException.class, () -> builder.setNumCpus(0));
+ assertThrows(IllegalArgumentException.class, () -> builder.setCpuTopology(-1));
assertThrows(IllegalArgumentException.class, () -> builder.setEncryptedStorageBytes(0));
// Consistency checks enforced at build time.
@@ -433,8 +546,6 @@
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void compatibleConfigTests() {
- int maxCpus = Runtime.getRuntime().availableProcessors();
-
VirtualMachineConfig baseline = newBaselineBuilder().build();
// A config must be compatible with itself
@@ -442,9 +553,9 @@
// Changes that must always be compatible
assertConfigCompatible(baseline, newBaselineBuilder().setMemoryBytes(99)).isTrue();
- if (maxCpus > 1) {
- assertConfigCompatible(baseline, newBaselineBuilder().setNumCpus(2)).isTrue();
- }
+ assertConfigCompatible(
+ baseline, newBaselineBuilder().setCpuTopology(CPU_TOPOLOGY_MATCH_HOST))
+ .isTrue();
// Changes that must be incompatible, since they must change the VM identity.
assertConfigCompatible(baseline, newBaselineBuilder().setDebugLevel(DEBUG_LEVEL_FULL))
@@ -525,7 +636,7 @@
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void vmmGetAndCreate() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -623,7 +734,7 @@
"9.17/C-1-4",
})
public void createVmWithConfigRequiresPermission() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -635,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");
}
@@ -645,7 +757,7 @@
"9.17/C-1-1",
})
public void deleteVm() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -673,7 +785,7 @@
"9.17/C-1-1",
})
public void deleteVmFiles() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -706,7 +818,7 @@
"9.17/C-1-1",
})
public void validApkPathIsAccepted() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -720,11 +832,12 @@
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mApkContentsPath = ts.getApkContentsPath();
});
- assertThat(testResults.mException).isNull();
+ testResults.assertNoException();
assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
}
@@ -742,7 +855,7 @@
"9.17/C-2-1"
})
public void extraApk() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig config =
@@ -755,6 +868,7 @@
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mExtraApkTestProp =
@@ -809,7 +923,7 @@
}
private void changeDebugLevel(int fromLevel, int toLevel) throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig.Builder builder =
newVmConfigBuilder()
@@ -847,15 +961,16 @@
private VmCdis launchVmAndGetCdis(String instanceName) throws Exception {
VirtualMachine vm = getVirtualMachineManager().get(instanceName);
- final VmCdis vmCdis = new VmCdis();
- final CompletableFuture<Exception> exception = new CompletableFuture<>();
+ VmCdis vmCdis = new VmCdis();
+ CompletableFuture<Exception> exception = new CompletableFuture<>();
VmEventListener listener =
new VmEventListener() {
@Override
public void onPayloadReady(VirtualMachine vm) {
try {
- ITestService testService = ITestService.Stub.asInterface(
- vm.connectToVsockServer(ITestService.SERVICE_PORT));
+ ITestService testService =
+ ITestService.Stub.asInterface(
+ vm.connectToVsockServer(ITestService.SERVICE_PORT));
vmCdis.cdiAttest = testService.insecurelyExposeAttestationCdi();
vmCdis.instanceSecret = testService.insecurelyExposeVmInstanceSecret();
} catch (Exception e) {
@@ -879,7 +994,7 @@
"9.17/C-2-7"
})
public void instancesOfSameVmHaveDifferentCdis() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig normalConfig =
@@ -905,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);
@@ -930,7 +1045,7 @@
"9.17/C-2-7"
})
public void bccIsSuperficiallyWellFormed() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig normalConfig =
@@ -939,26 +1054,15 @@
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("bcc_vm", normalConfig);
- final CompletableFuture<byte[]> bcc = new CompletableFuture<>();
- final CompletableFuture<Exception> exception = new CompletableFuture<>();
- VmEventListener listener =
- new VmEventListener() {
- @Override
- public void onPayloadReady(VirtualMachine vm) {
- try {
- ITestService testService = ITestService.Stub.asInterface(
- vm.connectToVsockServer(ITestService.SERVICE_PORT));
- bcc.complete(testService.getBcc());
- } catch (Exception e) {
- exception.complete(e);
- } finally {
- forceStop(vm);
- }
- }
- };
- listener.runToFinish(TAG, vm);
- byte[] bccBytes = bcc.getNow(null);
- assertThat(exception.getNow(null)).isNull();
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (service, results) -> {
+ results.mBcc = service.getBcc();
+ });
+ testResults.assertNoException();
+ byte[] bccBytes = testResults.mBcc;
assertThat(bccBytes).isNotNull();
ByteArrayInputStream bais = new ByteArrayInputStream(bccBytes);
@@ -980,7 +1084,7 @@
"9.17/C-1-2"
})
public void accessToCdisIsRestricted() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -995,10 +1099,6 @@
private static final UUID MICRODROID_PARTITION_UUID =
UUID.fromString("cf9afe9a-0662-11ec-a329-c32663a09d75");
- private static final UUID U_BOOT_AVB_PARTITION_UUID =
- UUID.fromString("7e8221e7-03e6-4969-948b-73a4c809a4f2");
- private static final UUID U_BOOT_ENV_PARTITION_UUID =
- UUID.fromString("0ab72d30-86ae-4d05-81b2-c1760be2b1f9");
private static final UUID PVM_FW_PARTITION_UUID =
UUID.fromString("90d2174a-038a-4bc6-adf3-824848fc5825");
private static final long BLOCK_SIZE = 512;
@@ -1184,7 +1284,7 @@
@Test
public void importedVmAndOriginalVmHaveTheSameCdi() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
// Arrange
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig config =
@@ -1197,7 +1297,6 @@
VirtualMachine vmOrig = forceCreateNewVirtualMachine(vmNameOrig, config);
VmCdis origCdis = launchVmAndGetCdis(vmNameOrig);
assertThat(origCdis.instanceSecret).isNotNull();
- VirtualMachineDescriptor descriptor = vmOrig.toDescriptor();
VirtualMachineManager vmm = getVirtualMachineManager();
if (vmm.get(vmNameImport) != null) {
vmm.delete(vmNameImport);
@@ -1205,7 +1304,7 @@
// Action
// The imported VM will be fetched by name later.
- VirtualMachine unusedVmImport = vmm.importFromDescriptor(vmNameImport, descriptor);
+ vmm.importFromDescriptor(vmNameImport, vmOrig.toDescriptor());
// Asserts
VmCdis importCdis = launchVmAndGetCdis(vmNameImport);
@@ -1213,14 +1312,14 @@
}
@Test
- @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+ @CddTest(requirements = {"9.17/C-1-1"})
public void importedVmIsEqualToTheOriginalVm_WithoutStorage() throws Exception {
TestResults testResults = importedVmIsEqualToTheOriginalVm(false);
assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
}
@Test
- @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+ @CddTest(requirements = {"9.17/C-1-1"})
public void importedVmIsEqualToTheOriginalVm_WithStorage() throws Exception {
TestResults testResults = importedVmIsEqualToTheOriginalVm(true);
assertThat(testResults.mEncryptedStoragePath).isEqualTo("/mnt/encryptedstore");
@@ -1243,21 +1342,21 @@
// 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);
tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
});
- assertThat(origTestResults.mException).isNull();
+ origTestResults.assertNoException();
assertThat(origTestResults.mAddInteger).isEqualTo(123 + 456);
- VirtualMachineDescriptor descriptor = vmOrig.toDescriptor();
VirtualMachineManager vmm = getVirtualMachineManager();
if (vmm.get(vmNameImport) != null) {
vmm.delete(vmNameImport);
}
// Action
- VirtualMachine vmImport = vmm.importFromDescriptor(vmNameImport, descriptor);
+ VirtualMachine vmImport = vmm.importFromDescriptor(vmNameImport, vmOrig.toDescriptor());
// Asserts
assertFileContentsAreEqualInTwoVms("config.xml", vmNameOrig, vmNameImport);
@@ -1270,20 +1369,21 @@
assertThat(vmImport).isEqualTo(vmm.get(vmNameImport));
TestResults testResults =
runVmTestService(
+ TAG,
vmImport,
(ts, tr) -> {
tr.mAddInteger = ts.addInteger(123, 456);
tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
});
- assertThat(testResults.mException).isNull();
+ testResults.assertNoException();
assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
return testResults;
}
@Test
- @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+ @CddTest(requirements = {"9.17/C-1-1"})
public void encryptedStorageAvailable() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -1296,6 +1396,7 @@
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
@@ -1304,9 +1405,9 @@
}
@Test
- @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+ @CddTest(requirements = {"9.17/C-1-1"})
public void encryptedStorageIsInaccessibleToDifferentVm() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -1314,20 +1415,20 @@
.setMemoryBytes(minMemoryRequired())
.setEncryptedStorageBytes(4_000_000)
.setDebugLevel(DEBUG_LEVEL_FULL)
- .setVmOutputCaptured(true)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
ts.writeToFile(
/* content= */ EXAMPLE_STRING,
/* path= */ "/mnt/encryptedstore/test_file");
});
- assertThat(testResults.mException).isNull();
+ testResults.assertNoException();
// Start a different vm (this changes the vm identity)
VirtualMachine diff_test_vm = forceCreateNewVirtualMachine("diff_test_vm", config);
@@ -1339,7 +1440,8 @@
assertFileContentsAreEqualInTwoVms("storage.img", "test_vm", "diff_test_vm");
CompletableFuture<Boolean> onPayloadReadyExecuted = new CompletableFuture<>();
- CompletableFuture<Boolean> onStoppedExecuted = new CompletableFuture<>();
+ CompletableFuture<Boolean> onErrorExecuted = new CompletableFuture<>();
+ CompletableFuture<String> errorMessage = new CompletableFuture<>();
VmEventListener listener =
new VmEventListener() {
@Override
@@ -1349,23 +1451,24 @@
}
@Override
- public void onStopped(VirtualMachine vm, int reason) {
- onStoppedExecuted.complete(true);
- super.onStopped(vm, reason);
+ public void onError(VirtualMachine vm, int errorCode, String message) {
+ onErrorExecuted.complete(true);
+ errorMessage.complete(message);
+ super.onError(vm, errorCode, message);
}
};
listener.runToFinish(TAG, diff_test_vm);
- // Assert that payload never started & logs contains encryptedstore initialization error
- assertThat(onStoppedExecuted.getNow(false)).isTrue();
+ // Assert that payload never started & error message reflects storage error.
assertThat(onPayloadReadyExecuted.getNow(false)).isFalse();
- assertThat(listener.getConsoleOutput()).contains("Unable to initialize encryptedstore");
+ assertThat(onErrorExecuted.getNow(false)).isTrue();
+ assertThat(errorMessage.getNow("")).contains("Unable to prepare encrypted storage");
}
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
public void microdroidLauncherHasEmptyCapabilities() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
final VirtualMachineConfig vmConfig =
newVmConfigBuilder()
@@ -1377,19 +1480,20 @@
final TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mEffectiveCapabilities = ts.getEffectiveCapabilities();
});
- assertThat(testResults.mException).isNull();
+ testResults.assertNoException();
assertThat(testResults.mEffectiveCapabilities).isEmpty();
}
@Test
- @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+ @CddTest(requirements = {"9.17/C-1-1"})
public void encryptedStorageIsPersistent() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -1401,30 +1505,32 @@
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_a", config);
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
ts.writeToFile(
/* content= */ EXAMPLE_STRING,
/* path= */ "/mnt/encryptedstore/test_file");
});
- assertThat(testResults.mException).isNull();
+ testResults.assertNoException();
// Re-run the same VM & verify the file persisted. Note, the previous `runVmTestService`
// stopped the VM
testResults =
runVmTestService(
+ TAG,
vm,
(ts, tr) -> {
tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/test_file");
});
- assertThat(testResults.mException).isNull();
+ testResults.assertNoException();
assertThat(testResults.mFileContent).isEqualTo(EXAMPLE_STRING);
}
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
public void canReadFileFromAssets_debugFull() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
VirtualMachineConfig config =
newVmConfigBuilder()
@@ -1436,21 +1542,22 @@
TestResults testResults =
runVmTestService(
+ TAG,
vm,
(testService, ts) -> {
ts.mFileContent = testService.readFromFile("/mnt/apk/assets/file.txt");
});
- assertThat(testResults.mException).isNull();
+ testResults.assertNoException();
assertThat(testResults.mFileContent).isEqualTo("Hello, I am a file!");
}
@Test
public void outputShouldBeExplicitlyCaptured() throws Exception {
- assumeSupportedKernel();
+ assumeSupportedDevice();
final VirtualMachineConfig vmConfig =
- new VirtualMachineConfig.Builder(ApplicationProvider.getApplicationContext())
+ new VirtualMachineConfig.Builder(getContext())
.setProtectedVm(mProtectedVm)
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
@@ -1468,11 +1575,27 @@
}
}
+ 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"));
final VirtualMachineConfig vmConfig =
- new VirtualMachineConfig.Builder(ApplicationProvider.getApplicationContext())
+ new VirtualMachineConfig.Builder(getContext())
.setProtectedVm(mProtectedVm)
.setPayloadBinaryName("MicrodroidTestNativeLib.so")
.setDebugLevel(debuggable ? DEBUG_LEVEL_FULL : DEBUG_LEVEL_NONE)
@@ -1480,14 +1603,7 @@
.build();
final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_logcat", vmConfig);
- VmEventListener listener =
- new VmEventListener() {
- @Override
- public void onPayloadStarted(VirtualMachine vm) {
- forceStop(vm);
- }
- };
- listener.runToFinish(TAG, vm);
+ runVmTestService(TAG, vm, (service, results) -> {});
// only check logs printed after this test
Process logcatProcess =
@@ -1507,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);
@@ -1536,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);
@@ -1546,6 +1669,318 @@
getVirtualMachineManager().delete("vm_from_another_app");
}
+ @Test
+ public void testVmDescriptorParcelUnparcel_noTrustedStorage() throws Exception {
+ assumeSupportedDevice();
+
+ VirtualMachineConfig config =
+ newVmConfigBuilder()
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .build();
+
+ VirtualMachine originalVm = forceCreateNewVirtualMachine("original_vm", config);
+ // Just start & stop the VM.
+ runVmTestService(TAG, originalVm, (ts, tr) -> {});
+
+ // Now create the descriptor and manually parcel & unparcel it.
+ VirtualMachineDescriptor vmDescriptor = toParcelFromParcel(originalVm.toDescriptor());
+
+ if (getVirtualMachineManager().get("import_vm_from_unparceled") != null) {
+ getVirtualMachineManager().delete("import_vm_from_unparceled");
+ }
+
+ VirtualMachine importVm =
+ getVirtualMachineManager()
+ .importFromDescriptor("import_vm_from_unparceled", vmDescriptor);
+
+ assertFileContentsAreEqualInTwoVms(
+ "config.xml", "original_vm", "import_vm_from_unparceled");
+ assertFileContentsAreEqualInTwoVms(
+ "instance.img", "original_vm", "import_vm_from_unparceled");
+
+ // Check that we can start and stop imported vm as well
+ runVmTestService(TAG, importVm, (ts, tr) -> {});
+ }
+
+ @Test
+ public void testVmDescriptorParcelUnparcel_withTrustedStorage() throws Exception {
+ assumeSupportedDevice();
+
+ VirtualMachineConfig config =
+ newVmConfigBuilder()
+ .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .setEncryptedStorageBytes(1_000_000)
+ .build();
+
+ VirtualMachine originalVm = forceCreateNewVirtualMachine("original_vm", config);
+ // Just start & stop the VM.
+ {
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ originalVm,
+ (ts, tr) -> {
+ ts.writeToFile("not a secret!", "/mnt/encryptedstore/secret.txt");
+ });
+ assertThat(testResults.mException).isNull();
+ }
+
+ // Now create the descriptor and manually parcel & unparcel it.
+ VirtualMachineDescriptor vmDescriptor = toParcelFromParcel(originalVm.toDescriptor());
+
+ if (getVirtualMachineManager().get("import_vm_from_unparceled") != null) {
+ getVirtualMachineManager().delete("import_vm_from_unparceled");
+ }
+
+ VirtualMachine importVm =
+ getVirtualMachineManager()
+ .importFromDescriptor("import_vm_from_unparceled", vmDescriptor);
+
+ assertFileContentsAreEqualInTwoVms(
+ "config.xml", "original_vm", "import_vm_from_unparceled");
+ assertFileContentsAreEqualInTwoVms(
+ "instance.img", "original_vm", "import_vm_from_unparceled");
+ assertFileContentsAreEqualInTwoVms(
+ "storage.img", "original_vm", "import_vm_from_unparceled");
+
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ importVm,
+ (ts, tr) -> {
+ tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/secret.txt");
+ });
+
+ assertThat(testResults.mException).isNull();
+ assertThat(testResults.mFileContent).isEqualTo("not a secret!");
+ }
+
+ @Test
+ public void testShareVmWithAnotherApp() throws Exception {
+ assumeSupportedDevice();
+
+ Context ctx = getContext();
+ Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
+
+ VirtualMachineConfig config =
+ new VirtualMachineConfig.Builder(otherAppCtx)
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .setProtectedVm(isProtectedVm())
+ .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
+ .build();
+
+ VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config);
+ // Just start & stop the VM.
+ runVmTestService(TAG, vm, (ts, tr) -> {});
+ // Get a descriptor that we will share with another app (VM_SHARE_APP_PACKAGE_NAME)
+ VirtualMachineDescriptor vmDesc = vm.toDescriptor();
+
+ Intent serviceIntent = new Intent();
+ serviceIntent.setComponent(
+ 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);
+ assertWithMessage("Failed to bind to " + serviceIntent).that(ret).isTrue();
+
+ IVmShareTestService service = connection.waitForService();
+ assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
+
+ try {
+ // Send the VM descriptor to the other app. When received, it will reconstruct the VM
+ // from the descriptor, start it, connect to the ITestService in it, creates a "proxy"
+ // ITestService binder that delegates all the calls to the VM, and share it with this
+ // app. It will allow us to verify assertions on the running VM in the other app.
+ ITestService testServiceProxy = service.startVm(vmDesc);
+
+ int result = testServiceProxy.addInteger(37, 73);
+ assertThat(result).isEqualTo(110);
+ } finally {
+ ctx.unbindService(connection);
+ }
+ }
+
+ @Test
+ public void testShareVmWithAnotherApp_encryptedStorage() throws Exception {
+ assumeSupportedDevice();
+
+ Context ctx = getContext();
+ Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0);
+
+ VirtualMachineConfig config =
+ new VirtualMachineConfig.Builder(otherAppCtx)
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .setProtectedVm(isProtectedVm())
+ .setEncryptedStorageBytes(3_000_000)
+ .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so")
+ .build();
+
+ 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");
+ });
+ // Get a descriptor that we will share with another app (VM_SHARE_APP_PACKAGE_NAME)
+ VirtualMachineDescriptor vmDesc = vm.toDescriptor();
+
+ Intent serviceIntent = new Intent();
+ serviceIntent.setComponent(
+ 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);
+ assertWithMessage("Failed to bind to " + serviceIntent).that(ret).isTrue();
+
+ IVmShareTestService service = connection.waitForService();
+ assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
+
+ try {
+ // Send the VM descriptor to the other app. When received, it will reconstruct the VM
+ // from the descriptor, start it, connect to the ITestService in it, creates a "proxy"
+ // ITestService binder that delegates all the calls to the VM, and share it with this
+ // app. It will allow us to verify assertions on the running VM in the other app.
+ ITestService testServiceProxy = service.startVm(vmDesc);
+
+ String result = testServiceProxy.readFromFile("/mnt/encryptedstore/private.key");
+ assertThat(result).isEqualTo(EXAMPLE_STRING);
+ } finally {
+ ctx.unbindService(connection);
+ }
+ }
+
+ @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);
+
+ private IVmShareTestService mVmShareTestService;
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mVmShareTestService = IVmShareTestService.Stub.asInterface(service);
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {}
+
+ private IVmShareTestService waitForService() throws Exception {
+ if (!mLatch.await(1, TimeUnit.MINUTES)) {
+ return null;
+ }
+ return mVmShareTestService;
+ }
+ }
+
+ private VirtualMachineDescriptor toParcelFromParcel(VirtualMachineDescriptor descriptor) {
+ Parcel parcel = Parcel.obtain();
+ descriptor.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ return VirtualMachineDescriptor.CREATOR.createFromParcel(parcel);
+ }
+
private void assertFileContentsAreEqualInTwoVms(String fileName, String vmName1, String vmName2)
throws IOException {
File file1 = getVmFile(vmName1, fileName);
@@ -1557,7 +1992,7 @@
}
private File getVmFile(String vmName, String fileName) {
- Context context = ApplicationProvider.getApplicationContext();
+ Context context = getContext();
Path filePath = Paths.get(context.getDataDir().getPath(), "vm", vmName, fileName);
return filePath.toFile();
}
@@ -1585,98 +2020,10 @@
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;
- }
-
- 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(VirtualMachine vm) {
- 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(vm);
- }
-
- @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/Android.bp b/tests/vmshareapp/Android.bp
index 2b117a1..6c2c9e4 100644
--- a/tests/vmshareapp/Android.bp
+++ b/tests/vmshareapp/Android.bp
@@ -5,6 +5,7 @@
// Helper app to verify that we can create a VM using others app payload, and share VMs between apps
android_test_helper_app {
name: "MicrodroidVmShareApp",
+ srcs: ["src/java/**/*.java"],
// Defaults are defined in ../testapk/Android.bp
defaults: ["MicrodroidTestAppsDefaults"],
jni_libs: [
diff --git a/tests/vmshareapp/AndroidManifest.xml b/tests/vmshareapp/AndroidManifest.xml
index eed3364..b623f7f 100644
--- a/tests/vmshareapp/AndroidManifest.xml
+++ b/tests/vmshareapp/AndroidManifest.xml
@@ -20,5 +20,13 @@
<uses-feature android:name="android.software.virtualization_framework"
android:required="false" />
- <application />
+ <application>
+ <service android:name="com.android.microdroid.test.sharevm.VmShareServiceImpl"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="com.android.microdroid.test.sharevm.VmShareService"/>
+ </intent-filter>
+ </service>
+ </application>
+
</manifest>
diff --git a/tests/vmshareapp/aidl/Android.bp b/tests/vmshareapp/aidl/Android.bp
new file mode 100644
index 0000000..df4a4b4
--- /dev/null
+++ b/tests/vmshareapp/aidl/Android.bp
@@ -0,0 +1,15 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Unfortunatelly aidl_interface doesn't work well with .aidl files that depend on java-only
+// parcelables (e.g. Bundle, VirtualMachineDescriptor), hence this java_library.
+java_library {
+ name: "com.android.microdroid.test.vmshare_service-java",
+ srcs: ["com/**/*.aidl"],
+ sdk_version: "test_current",
+ static_libs: ["com.android.microdroid.testservice-java"],
+ aidl: {
+ include_dirs: ["packages/modules/Virtualization/tests/aidl/"],
+ },
+}
diff --git a/libs/dice/src/lib.rs b/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
similarity index 62%
copy from libs/dice/src/lib.rs
copy to tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
index 6870eeb..fe6ca43 100644
--- a/libs/dice/src/lib.rs
+++ b/tests/vmshareapp/aidl/com/android/microdroid/test/vmshare/IVmShareTestService.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * 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.
@@ -13,14 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.microdroid.test.vmshare;
-//! Bare metal wrapper around libopen_dice.
+import android.system.virtualmachine.VirtualMachineDescriptor;
+import com.android.microdroid.testservice.ITestService;
-#![no_std]
-
-pub use diced_open_dice::{
- bcc_format_config_descriptor, check_result, Cdi, Config, DiceError, DiceMode, Hash,
- InputValues, Result, CDI_SIZE, HASH_SIZE, HIDDEN_SIZE,
-};
-
-pub mod bcc;
+/** {@hide} */
+interface IVmShareTestService {
+ ITestService startVm(in VirtualMachineDescriptor vmDesc);
+}
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
new file mode 100644
index 0000000..edd6bf5
--- /dev/null
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 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.test.sharevm;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCallback;
+import android.system.virtualmachine.VirtualMachineDescriptor;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
+import android.util.Log;
+
+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;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A {@link Service} that is used in end-to-end tests of the {@link VirtualMachine} sharing
+ * functionality.
+ *
+ * <p>During the test {@link com.android.microdroid.test.MicrodroidTests} will bind to this service,
+ * and call {@link #startVm(VirtualMachineDescriptor)} to share the VM. This service then will
+ * create a {@link VirtualMachine} from that descriptor, {@link VirtualMachine#run() run} it, and
+ * send back {@link RemoteTestServiceDelegate}. The {@code MicrodroidTests} can use that {@link
+ * RemoteTestServiceDelegate} to assert conditions on the VM running in the {@link
+ * VmShareServiceImpl}.
+ *
+ * <p>The {@link VirtualMachine} running in this service will be stopped on {@link
+ * #onUnbind(Intent)}.
+ *
+ * @see com.android.microdroid.test.MicrodroidTests#testShareVmWithAnotherApp
+ */
+public class VmShareServiceImpl extends Service {
+
+ private static final String TAG = "VmShareApp";
+
+ private IVmShareTestService.Stub mBinder;
+
+ private VirtualMachine mVirtualMachine;
+
+ @Override
+ public void onCreate() {
+ mBinder = new ServiceImpl();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.i(TAG, "onBind " + intent + " binder = " + mBinder);
+ return mBinder;
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ deleteVm();
+ // Tell framework that it shouldn't call onRebind.
+ return false;
+ }
+
+ private void deleteVm() {
+ if (mVirtualMachine == null) {
+ return;
+ }
+ try {
+ mVirtualMachine.stop();
+ String name = mVirtualMachine.getName();
+ VirtualMachineManager vmm = getSystemService(VirtualMachineManager.class);
+ vmm.delete(name);
+ mVirtualMachine = null;
+ } catch (VirtualMachineException e) {
+ Log.e(TAG, "Failed to stop " + mVirtualMachine, e);
+ }
+ }
+
+ public ITestService startVm(VirtualMachineDescriptor vmDesc) throws Exception {
+ // Cleanup VM left from the previous test.
+ deleteVm();
+
+ VirtualMachineManager vmm = getSystemService(VirtualMachineManager.class);
+
+ // Add random uuid to make sure that different tests that bind to this service don't trip
+ // over each other.
+ String vmName = "imported_vm" + UUID.randomUUID();
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ VirtualMachineCallback callback =
+ new VirtualMachineCallback() {
+
+ @Override
+ public void onPayloadStarted(VirtualMachine vm) {
+ // Ignored
+ }
+
+ @Override
+ public void onPayloadReady(VirtualMachine vm) {
+ latch.countDown();
+ }
+
+ @Override
+ public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+ // Ignored
+ }
+
+ @Override
+ public void onError(VirtualMachine vm, int errorCode, String message) {
+ throw new RuntimeException(
+ "VM failed with error " + errorCode + " : " + message);
+ }
+
+ @Override
+ public void onStopped(VirtualMachine vm, int reason) {
+ // Ignored
+ }
+ };
+
+ mVirtualMachine = vmm.importFromDescriptor(vmName, vmDesc);
+ mVirtualMachine.setCallback(getMainExecutor(), callback);
+
+ Log.i(TAG, "Starting VM " + vmName);
+ mVirtualMachine.run();
+ if (!latch.await(1, TimeUnit.MINUTES)) {
+ throw new TimeoutException("Timed out starting VM");
+ }
+
+ Log.i(
+ TAG,
+ "Payload is ready, connecting to the vsock service at port "
+ + ITestService.SERVICE_PORT);
+ ITestService testService =
+ ITestService.Stub.asInterface(
+ mVirtualMachine.connectToVsockServer(ITestService.SERVICE_PORT));
+ return new RemoteTestServiceDelegate(testService);
+ }
+
+ final class ServiceImpl extends IVmShareTestService.Stub {
+
+ @Override
+ public ITestService startVm(VirtualMachineDescriptor vmDesc) {
+ Log.i(TAG, "startVm binder call received");
+ try {
+ return VmShareServiceImpl.this.startVm(vmDesc);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to startVm", e);
+ throw new IllegalStateException("Failed to startVm", e);
+ }
+ }
+ }
+
+ private static class RemoteTestServiceDelegate extends ITestService.Stub {
+
+ private final ITestService mServiceInVm;
+
+ private RemoteTestServiceDelegate(ITestService serviceInVm) {
+ mServiceInVm = serviceInVm;
+ }
+
+ @Override
+ public int addInteger(int a, int b) throws RemoteException {
+ return mServiceInVm.addInteger(a, b);
+ }
+
+ @Override
+ public String readProperty(String prop) throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public byte[] insecurelyExposeVmInstanceSecret() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public byte[] insecurelyExposeAttestationCdi() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public byte[] getBcc() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public String getApkContentsPath() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public String getEncryptedStoragePath() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public void runEchoReverseServer() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public String[] getEffectiveCapabilities() throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public void writeToFile(String content, String path) throws RemoteException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public String readFromFile(String path) throws RemoteException {
+ return mServiceInVm.readFromFile(path);
+ }
+
+ @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 a436cea..c913d02 100644
--- a/virtualizationmanager/Android.bp
+++ b/virtualizationmanager/Android.bp
@@ -32,6 +32,7 @@
"libclap",
"libcommand_fds",
"libdisk",
+ "libhypervisor_props",
"liblazy_static",
"liblibc",
"liblog_rust",
@@ -59,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 678c91f..749d75f 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;
@@ -27,6 +29,7 @@
ErrorCode::ErrorCode,
};
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+ CpuTopology::CpuTopology,
DiskImage::DiskImage,
IVirtualMachine::{BnVirtualMachine, IVirtualMachine},
IVirtualMachineCallback::IVirtualMachineCallback,
@@ -45,7 +48,7 @@
use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
BnVirtualMachineService, IVirtualMachineService,
};
-use anyhow::{bail, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
use apkverify::{HashAlgorithm, V4Signature};
use binder::{
self, wait_for_interface, BinderFeatures, ExceptionCode, Interface, ParcelFileDescriptor,
@@ -57,12 +60,13 @@
use microdroid_payload_config::{OsConfig, Task, TaskType, VmPayloadConfig};
use nix::unistd::pipe;
use rpcbinder::RpcServer;
+use rustutils::system_properties;
use semver::VersionReq;
use std::convert::TryInto;
use std::ffi::CStr;
use std::fs::{read_dir, remove_file, File, OpenOptions};
use std::io::{BufRead, BufReader, Error, ErrorKind, Write};
-use std::num::NonZeroU32;
+use std::num::{NonZeroU16, NonZeroU32};
use std::os::unix::io::{FromRawFd, IntoRawFd};
use std::os::unix::raw::pid_t;
use std::path::{Path, PathBuf};
@@ -108,8 +112,9 @@
if !metadata.is_file() {
bail!("input is not a regular file");
}
- let mut sig = V4Signature::create(&mut input, 4096, &[], HashAlgorithm::SHA256)
- .context("failed to create idsig")?;
+ let mut sig =
+ V4Signature::create(&mut input, get_current_sdk()?, 4096, &[], HashAlgorithm::SHA256)
+ .context("failed to create idsig")?;
let mut output = clone_file(idsig_fd)?;
output.set_len(0).context("failed to set_len on the idsig output")?;
@@ -117,6 +122,12 @@
Ok(())
}
+fn get_current_sdk() -> Result<u32> {
+ let current_sdk = system_properties::read("ro.build.version.sdk")?;
+ let current_sdk = current_sdk.ok_or_else(|| anyhow!("SDK version missing"))?;
+ current_sdk.parse().context("Malformed SDK version")
+}
+
pub fn remove_temporary_files(path: &PathBuf) -> Result<()> {
for dir_entry in read_dir(path)? {
remove_file(dir_entry?.path())?;
@@ -298,14 +309,35 @@
// Some features are reserved for platform apps only, even when using
// VirtualMachineAppConfig:
// - controlling CPUs;
- // - specifying a config file in the APK.
- !config.taskProfiles.is_empty() || matches!(config.payload, Payload::ConfigPath(_))
+ // - specifying a config file in the APK;
+ // - gdbPort is set, meaning that crosvm will start a gdb server.
+ !config.taskProfiles.is_empty()
+ || matches!(config.payload, Payload::ConfigPath(_))
+ || config.gdbPort > 0
}
};
if is_custom {
check_use_custom_virtual_machine()?;
}
+ let gdb_port = extract_gdb_port(config);
+
+ // Additional permission checks if caller request gdb.
+ if gdb_port.is_some() {
+ 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))?;
@@ -384,18 +416,17 @@
})
.collect::<Result<Vec<DiskFile>, _>>()?;
- // 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)),
- )
- })?;
+ let (cpus, host_cpu_topology) = match config.cpuTopology {
+ CpuTopology::MATCH_HOST => (None, true),
+ CpuTopology::ONE_CPU => (NonZeroU32::new(1), false),
+ val => {
+ error!("Unexpected value of CPU topology: {:?}", val);
+ return Err(Status::new_service_specific_error_str(
+ -1,
+ Some(format!("Failed to parse CPU topology value: {:?}", val)),
+ ));
+ }
+ };
// Actually start the VM.
let crosvm_config = CrosvmConfig {
@@ -407,15 +438,18 @@
disks,
params: config.params.to_owned(),
protected: *is_protected,
+ debug_level,
memory_mib: config.memoryMib.try_into().ok().and_then(NonZeroU32::new),
- cpus: config.numCpus.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,
+ gdb_port,
};
let instance = Arc::new(
VmInstance::new(
@@ -460,10 +494,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;
@@ -567,8 +597,9 @@
vm_config.name = config.name.clone();
vm_config.protectedVm = config.protectedVm;
- vm_config.numCpus = config.numCpus;
+ vm_config.cpuTopology = config.cpuTopology;
vm_config.taskProfiles = config.taskProfiles.clone();
+ vm_config.gdbPort = config.gdbPort;
// Microdroid takes additional init ramdisk & (optionally) storage image
add_microdroid_system_images(config, instance_file, storage_image, &mut vm_config)?;
@@ -694,10 +725,11 @@
/// user devices (W^X).
fn check_label_is_allowed(context: &SeContext) -> Result<()> {
match context.selinux_type()? {
- | "system_file" // immutable dm-verity protected partition
| "apk_data_file" // APKs of an installed app
- | "staging_data_file" // updated/staged APEX images
| "shell_data_file" // test files created via adb shell
+ | "staging_data_file" // updated/staged APEX images
+ | "system_file" // immutable dm-verity protected partition
+ | "virtualizationservice_data_file" // files created by VS / VirtMgr
=> Ok(()),
_ => bail!("Label {} is not allowed", context),
}
@@ -952,10 +984,56 @@
})
}
-fn is_debuggable(config: &VirtualMachineConfig) -> bool {
+/// 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 {
match config {
- VirtualMachineConfig::AppConfig(config) => config.debugLevel != DebugLevel::NONE,
- _ => false,
+ VirtualMachineConfig::RawConfig(config) => config.protectedVm,
+ VirtualMachineConfig::AppConfig(config) => config.protectedVm,
+ }
+}
+
+fn check_gdb_allowed(config: &VirtualMachineConfig) -> binder::Result<()> {
+ if is_protected(config) {
+ return Err(Status::new_exception_str(
+ ExceptionCode::SECURITY,
+ Some("can't use gdb with protected VMs"),
+ ));
+ }
+
+ match config {
+ VirtualMachineConfig::RawConfig(_) => Ok(()),
+ VirtualMachineConfig::AppConfig(config) => {
+ if config.debugLevel != DebugLevel::FULL {
+ Err(Status::new_exception_str(
+ ExceptionCode::SECURITY,
+ Some("can't use gdb with non-debuggable VMs"),
+ ))
+ } else {
+ Ok(())
+ }
+ }
+ }
+}
+
+fn extract_gdb_port(config: &VirtualMachineConfig) -> Option<NonZeroU16> {
+ match config {
+ VirtualMachineConfig::RawConfig(config) => NonZeroU16::new(config.gdbPort as u16),
+ VirtualMachineConfig::AppConfig(config) => NonZeroU16::new(config.gdbPort as u16),
}
}
@@ -968,9 +1046,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/atom.rs b/virtualizationmanager/src/atom.rs
index c33f262..567fce9 100644
--- a/virtualizationmanager/src/atom.rs
+++ b/virtualizationmanager/src/atom.rs
@@ -19,6 +19,7 @@
use crate::get_calling_uid;
use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::DeathReason::DeathReason;
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+ CpuTopology::CpuTopology,
IVirtualMachine::IVirtualMachine,
VirtualMachineAppConfig::{Payload::Payload, VirtualMachineAppConfig},
VirtualMachineConfig::VirtualMachineConfig,
@@ -38,6 +39,8 @@
use std::time::{Duration, SystemTime};
use zip::ZipArchive;
+const INVALID_NUM_CPUS: i32 = -1;
+
fn get_apex_list(config: &VirtualMachineAppConfig) -> String {
match &config.payload {
Payload::PayloadConfig(_) => String::new(),
@@ -76,6 +79,19 @@
}
}
+// Returns the number of CPUs configured in the host system.
+// This matches how crosvm determines the number of logical cores.
+// For telemetry purposes only.
+pub(crate) fn get_num_cpus() -> Option<usize> {
+ // SAFETY - Only integer constants passed back and forth.
+ let ret = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_CONF) };
+ if ret > 0 {
+ ret.try_into().ok()
+ } else {
+ None
+ }
+}
+
/// Write the stats of VMCreation to statsd
pub fn write_vm_creation_stats(
config: &VirtualMachineConfig,
@@ -94,23 +110,33 @@
binder_exception_code = e.exception_code() as i32;
}
}
- let (vm_identifier, config_type, num_cpus, memory_mib, apexes) = match config {
+ let (vm_identifier, config_type, cpu_topology, memory_mib, apexes) = match config {
VirtualMachineConfig::AppConfig(config) => (
config.name.clone(),
vm_creation_requested::ConfigType::VirtualMachineAppConfig,
- config.numCpus,
+ config.cpuTopology,
config.memoryMib,
get_apex_list(config),
),
VirtualMachineConfig::RawConfig(config) => (
config.name.clone(),
vm_creation_requested::ConfigType::VirtualMachineRawConfig,
- config.numCpus,
+ config.cpuTopology,
config.memoryMib,
String::new(),
),
};
+ let num_cpus: i32 = match cpu_topology {
+ CpuTopology::MATCH_HOST => {
+ get_num_cpus().and_then(|v| v.try_into().ok()).unwrap_or_else(|| {
+ warn!("Failed to determine the number of CPUs in the host");
+ INVALID_NUM_CPUS
+ })
+ }
+ _ => 1,
+ };
+
let atom = AtomVmCreationRequested {
uid: get_calling_uid() as i32,
vmIdentifier: vm_identifier,
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 19d862a..9db0971 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -15,7 +15,8 @@
//! Functions for running instances of `crosvm`.
use crate::aidl::{remove_temporary_files, Cid, VirtualMachineCallbacks};
-use crate::atom::write_vm_exited_stats;
+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;
@@ -32,7 +33,7 @@
use std::fs::{read_to_string, File};
use std::io::{self, Read};
use std::mem;
-use std::num::NonZeroU32;
+use std::num::{NonZeroU16, NonZeroU32};
use std::os::unix::io::{AsRawFd, RawFd, FromRawFd};
use std::os::unix::process::ExitStatusExt;
use std::path::{Path, PathBuf};
@@ -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,8 +101,10 @@
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,
pub task_profiles: Vec<String>,
pub console_fd: Option<File>,
pub log_fd: Option<File>,
@@ -104,6 +112,7 @@
pub indirect_files: Vec<File>,
pub platform_version: VersionReq,
pub detect_hangup: bool,
+ pub gdb_port: Option<NonZeroU16>,
}
/// A disk image to pass to crosvm for a VM.
@@ -528,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)?;
}
@@ -663,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,
@@ -720,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 {
@@ -732,10 +750,23 @@
command.arg("--cpus").arg(cpus.to_string());
}
+ if config.host_cpu_topology {
+ // TODO(b/266664564): replace with --host-cpu-topology once available
+ if let Some(cpus) = get_num_cpus() {
+ command.arg("--cpus").arg(cpus.to_string());
+ } else {
+ bail!("Could not determine the number of CPUs in the system");
+ }
+ }
+
if !config.task_profiles.is_empty() {
command.arg("--task-profiles").arg(config.task_profiles.join(","));
}
+ if let Some(gdb_port) = config.gdb_port {
+ command.arg("--gdb").arg(gdb_port.to_string());
+ }
+
// Keep track of what file descriptors should be mapped to the crosvm process.
let mut preserved_fds = config.indirect_files.iter().map(|file| file.as_raw_fd()).collect();
@@ -797,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..666c98d
--- /dev/null
+++ b/virtualizationmanager/src/debug_config.rs
@@ -0,0 +1,59 @@
+// 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()
+ || get_debug_policy_bool("/proc/device-tree/avf/guest/microdroid/adb").unwrap_or_default()
+}
+
+/// Get whether debug apexes (MICRODROID_REQUIRED_APEXES_DEBUG) are required.
+pub fn should_include_debug_apexes(debug_level: DebugLevel) -> bool {
+ debug_level != DebugLevel::NONE
+ || get_debug_policy_bool("/proc/device-tree/avf/guest/microdroid/adb").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 dca64cb..bd7f8af 100644
--- a/virtualizationmanager/src/main.rs
+++ b/virtualizationmanager/src/main.rs
@@ -18,12 +18,13 @@
mod atom;
mod composite;
mod crosvm;
+mod debug_config;
mod payload;
mod selinux;
use crate::aidl::{GLOBAL_SERVICE, VirtualizationService};
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::BnVirtualizationService;
-use anyhow::{bail, Context};
+use anyhow::{bail, Context, Result};
use binder::{BinderFeatures, ProcessState};
use lazy_static::lazy_static;
use log::{info, Level};
@@ -33,7 +34,6 @@
use nix::fcntl::{fcntl, F_GETFD, F_SETFD, FdFlag};
use nix::unistd::{Pid, Uid};
use std::os::unix::raw::{pid_t, uid_t};
-use rustutils::system_properties;
const LOG_TAG: &str = "virtmgr";
@@ -92,9 +92,15 @@
Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
}
-fn is_property_set(name: &str) -> bool {
- system_properties::read_bool(name, false)
- .unwrap_or_else(|e| panic!("Failed to read {name}: {e:?}"))
+fn check_vm_support() -> Result<()> {
+ if hypervisor_props::is_any_vm_supported()? {
+ Ok(())
+ } else {
+ // This should never happen, it indicates a misconfigured device where the virt APEX
+ // is present but VMs are not supported. If it does happen, fail fast to avoid wasting
+ // resources trying.
+ bail!("Device doesn't support protected or non-protected VMs")
+ }
}
fn main() {
@@ -105,14 +111,7 @@
.with_log_id(android_logger::LogId::System),
);
- let non_protected_vm_supported = is_property_set("ro.boot.hypervisor.vm.supported");
- let protected_vm_supported = is_property_set("ro.boot.hypervisor.protected_vm.supported");
- if !non_protected_vm_supported && !protected_vm_supported {
- // This should never happen, it indicates a misconfigured device where the virt APEX
- // is present but VMs are not supported. If it does happen, fail fast to avoid wasting
- // resources trying.
- panic!("Device doesn't support protected or unprotected VMs");
- }
+ check_vm_support().unwrap();
let args = Args::parse();
diff --git a/virtualizationmanager/src/payload.rs b/virtualizationmanager/src/payload.rs
index 02e8f8e..99aea01 100644
--- a/virtualizationmanager/src/payload.rs
+++ b/virtualizationmanager/src/payload.rs
@@ -14,6 +14,7 @@
//! Payload disk image
+use crate::debug_config::should_include_debug_apexes;
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
DiskImage::DiskImage,
Partition::Partition,
@@ -382,7 +383,7 @@
debug_level: DebugLevel,
) -> Vec<&'a ApexInfo> {
let mut additional_apexes: Vec<&str> = MICRODROID_REQUIRED_APEXES.to_vec();
- if debug_level != DebugLevel::NONE {
+ if should_include_debug_apexes(debug_level) {
additional_apexes.extend(MICRODROID_REQUIRED_APEXES_DEBUG.to_vec());
}
diff --git a/libs/dice/src/lib.rs b/virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl
similarity index 64%
rename from libs/dice/src/lib.rs
rename to virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl
index 6870eeb..8a8e3d0 100644
--- a/libs/dice/src/lib.rs
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * 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.
@@ -13,14 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package android.system.virtualizationservice;
-//! Bare metal wrapper around libopen_dice.
-
-#![no_std]
-
-pub use diced_open_dice::{
- bcc_format_config_descriptor, check_result, Cdi, Config, DiceError, DiceMode, Hash,
- InputValues, Result, CDI_SIZE, HASH_SIZE, HIDDEN_SIZE,
-};
-
-pub mod bcc;
+/** The vCPU topology that will be generated for the VM. */
+@Backing(type="byte")
+enum CpuTopology {
+ /** One vCPU */
+ ONE_CPU = 0,
+ /** Match physical CPU topology of the host. */
+ MATCH_HOST = 1,
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index 884561d..c467c2f 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -15,6 +15,7 @@
*/
package android.system.virtualizationservice;
+import android.system.virtualizationservice.CpuTopology;
import android.system.virtualizationservice.VirtualMachinePayloadConfig;
/** Configuration for running an App in a VM */
@@ -69,6 +70,12 @@
/** Debug level of the VM */
DebugLevel debugLevel = DebugLevel.NONE;
+ /**
+ * Port at which crosvm will start a gdb server to debug guest kernel.
+ * If set to zero, then gdb server won't be started.
+ */
+ int gdbPort = 0;
+
/** Whether the VM should be a protected VM. */
boolean protectedVm;
@@ -78,10 +85,8 @@
*/
int memoryMib;
- /**
- * Number of vCPUs in the VM. Defaults to 1.
- */
- int numCpus = 1;
+ /** The vCPU topology that will be generated for the VM. Default to 1 vCPU. */
+ CpuTopology cpuTopology = CpuTopology.ONE_CPU;
/**
* List of task profile names to apply for the VM
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 993bbb0..87d4ba2 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -15,6 +15,7 @@
*/
package android.system.virtualizationservice;
+import android.system.virtualizationservice.CpuTopology;
import android.system.virtualizationservice.DiskImage;
/** Raw configuration for running a VM. */
@@ -49,10 +50,8 @@
/** The amount of RAM to give the VM, in MiB. 0 or negative to use the default. */
int memoryMib;
- /**
- * Number of vCPUs in the VM. Defaults to 1.
- */
- int numCpus = 1;
+ /** The vCPU topology that will be generated for the VM. Default to 1 vCPU. */
+ CpuTopology cpuTopology = CpuTopology.ONE_CPU;
/**
* A version or range of versions of the virtual platform that this config is compatible with.
@@ -64,4 +63,10 @@
* List of task profile names to apply for the VM
*/
String[] taskProfiles;
+
+ /**
+ * Port at which crosvm will start a gdb server to debug guest kernel.
+ * If set to zero, then gdb server won't be started.
+ */
+ int gdbPort = 0;
}
diff --git a/vm/Android.bp b/vm/Android.bp
index e217786..50e68cc 100644
--- a/vm/Android.bp
+++ b/vm/Android.bp
@@ -15,11 +15,11 @@
"libclap",
"libenv_logger",
"libglob",
+ "libhypervisor_props",
"liblibc",
"liblog_rust",
"libmicrodroid_payload_config",
"librand",
- "librustutils",
"libserde_json",
"libserde",
"libvmconfig",
diff --git a/vm/src/main.rs b/vm/src/main.rs
index ea744f7..1d9f50b 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -19,16 +19,16 @@
mod run;
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
- IVirtualizationService::IVirtualizationService, PartitionType::PartitionType,
- VirtualMachineAppConfig::DebugLevel::DebugLevel,
+ CpuTopology::CpuTopology, IVirtualizationService::IVirtualizationService,
+ PartitionType::PartitionType, VirtualMachineAppConfig::DebugLevel::DebugLevel,
};
use anyhow::{Context, Error};
-use binder::ProcessState;
+use binder::{ProcessState, Strong};
use clap::Parser;
use create_idsig::command_create_idsig;
use create_partition::command_create_partition;
use run::{command_run, command_run_app, command_run_microdroid};
-use rustutils::system_properties;
+use std::num::NonZeroU16;
use std::path::{Path, PathBuf};
#[derive(Debug)]
@@ -91,9 +91,9 @@
#[clap(short, long)]
mem: Option<u32>,
- /// Number of vCPUs in the VM. If unspecified, defaults to 1.
- #[clap(long)]
- cpus: Option<u32>,
+ /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
+ #[clap(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
+ cpu_topology: CpuTopology,
/// Comma separated list of task profile names to apply to the VM
#[clap(long)]
@@ -102,6 +102,11 @@
/// Paths to extra idsig files.
#[clap(long = "extra-idsig")]
extra_idsigs: Vec<PathBuf>,
+
+ /// Port at which crosvm will start a gdb server to debug guest kernel.
+ /// Note: this is only supported on Android kernels android14-5.15 and higher.
+ #[clap(long)]
+ gdb: Option<NonZeroU16>,
},
/// Run a virtual machine with Microdroid inside
RunMicrodroid {
@@ -146,13 +151,18 @@
#[clap(short, long)]
mem: Option<u32>,
- /// Number of vCPUs in the VM. If unspecified, defaults to 1.
- #[clap(long)]
- cpus: Option<u32>,
+ /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
+ #[clap(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
+ cpu_topology: CpuTopology,
/// Comma separated list of task profile names to apply to the VM
#[clap(long)]
task_profiles: Vec<String>,
+
+ /// Port at which crosvm will start a gdb server to debug guest kernel.
+ /// Note: this is only supported on Android kernels android14-5.15 and higher.
+ #[clap(long)]
+ gdb: Option<NonZeroU16>,
},
/// Run a virtual machine
Run {
@@ -163,9 +173,9 @@
#[clap(long)]
name: Option<String>,
- /// Number of vCPUs in the VM. If unspecified, defaults to 1.
- #[clap(long)]
- cpus: Option<u32>,
+ /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
+ #[clap(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
+ cpu_topology: CpuTopology,
/// Comma separated list of task profile names to apply to the VM
#[clap(long)]
@@ -178,6 +188,11 @@
/// Path to file for VM log output.
#[clap(long)]
log: Option<PathBuf>,
+
+ /// Port at which crosvm will start a gdb server to debug guest kernel.
+ /// Note: this is only supported on Android kernels android14-5.15 and higher.
+ #[clap(long)]
+ gdb: Option<NonZeroU16>,
},
/// List running virtual machines
List,
@@ -222,6 +237,20 @@
}
}
+fn parse_cpu_topology(s: &str) -> Result<CpuTopology, String> {
+ match s {
+ "one_cpu" => Ok(CpuTopology::ONE_CPU),
+ "match_host" => Ok(CpuTopology::MATCH_HOST),
+ _ => Err(format!("Invalid cpu topology {}", s)),
+ }
+}
+
+fn get_service() -> Result<Strong<dyn IVirtualizationService>, Error> {
+ let virtmgr =
+ vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
+ virtmgr.connect().context("Failed to connect to VirtualizationService")
+}
+
fn main() -> Result<(), Error> {
env_logger::init();
let opt = Opt::parse();
@@ -229,10 +258,6 @@
// We need to start the thread pool for Binder to work properly, especially link_to_death.
ProcessState::start_thread_pool();
- let virtmgr =
- vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
- let service = virtmgr.connect().context("Failed to connect to VirtualizationService")?;
-
match opt {
Opt::RunApp {
name,
@@ -248,12 +273,13 @@
debug,
protected,
mem,
- cpus,
+ cpu_topology,
task_profiles,
extra_idsigs,
+ gdb,
} => command_run_app(
name,
- service.as_ref(),
+ get_service()?.as_ref(),
&apk,
&idsig,
&instance,
@@ -266,9 +292,10 @@
debug,
protected,
mem,
- cpus,
+ cpu_topology,
task_profiles,
&extra_idsigs,
+ gdb,
),
Opt::RunMicrodroid {
name,
@@ -280,11 +307,12 @@
debug,
protected,
mem,
- cpus,
+ cpu_topology,
task_profiles,
+ gdb,
} => command_run_microdroid(
name,
- service.as_ref(),
+ get_service()?.as_ref(),
work_dir,
storage.as_deref(),
storage_size,
@@ -293,27 +321,31 @@
debug,
protected,
mem,
- cpus,
+ cpu_topology,
task_profiles,
+ gdb,
),
- Opt::Run { name, config, cpus, task_profiles, console, log } => {
+ Opt::Run { name, config, cpu_topology, task_profiles, console, log, gdb } => {
command_run(
name,
- service.as_ref(),
+ get_service()?.as_ref(),
&config,
console.as_deref(),
log.as_deref(),
/* mem */ None,
- cpus,
+ cpu_topology,
task_profiles,
+ gdb,
)
}
- Opt::List => command_list(service.as_ref()),
+ Opt::List => command_list(get_service()?.as_ref()),
Opt::Info => command_info(),
Opt::CreatePartition { path, size, partition_type } => {
- command_create_partition(service.as_ref(), &path, size, partition_type)
+ command_create_partition(get_service()?.as_ref(), &path, size, partition_type)
}
- Opt::CreateIdsig { apk, path } => command_create_idsig(service.as_ref(), &apk, &path),
+ Opt::CreateIdsig { apk, path } => {
+ command_create_idsig(get_service()?.as_ref(), &apk, &path)
+ }
}
}
@@ -326,10 +358,8 @@
/// Print information about supported VM types.
fn command_info() -> Result<(), Error> {
- let non_protected_vm_supported =
- system_properties::read_bool("ro.boot.hypervisor.vm.supported", false)?;
- let protected_vm_supported =
- system_properties::read_bool("ro.boot.hypervisor.protected_vm.supported", false)?;
+ let non_protected_vm_supported = hypervisor_props::is_vm_supported()?;
+ let protected_vm_supported = hypervisor_props::is_protected_vm_supported()?;
match (non_protected_vm_supported, protected_vm_supported) {
(false, false) => println!("VMs are not supported."),
(false, true) => println!("Only protected VMs are supported."),
@@ -337,7 +367,7 @@
(true, true) => println!("Both protected and non-protected VMs are supported."),
}
- if let Some(version) = system_properties::read("ro.boot.hypervisor.version")? {
+ if let Some(version) = hypervisor_props::version()? {
println!("Hypervisor version: {}", version);
} else {
println!("Hypervisor version not set.");
diff --git a/vm/src/run.rs b/vm/src/run.rs
index e229933..36edc64 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -16,6 +16,7 @@
use crate::create_partition::command_create_partition;
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+ CpuTopology::CpuTopology,
IVirtualizationService::IVirtualizationService,
PartitionType::PartitionType,
VirtualMachineAppConfig::{DebugLevel::DebugLevel, Payload::Payload, VirtualMachineAppConfig},
@@ -31,6 +32,7 @@
use std::fs;
use std::fs::File;
use std::io;
+use std::num::NonZeroU16;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::path::{Path, PathBuf};
use vmclient::{ErrorCode, VmInstance};
@@ -54,9 +56,10 @@
debug_level: DebugLevel,
protected: bool,
mem: Option<u32>,
- cpus: Option<u32>,
+ cpu_topology: CpuTopology,
task_profiles: Vec<String>,
extra_idsigs: &[PathBuf],
+ gdb: Option<NonZeroU16>,
) -> Result<(), Error> {
let apk_file = File::open(apk).context("Failed to open APK file")?;
@@ -141,14 +144,15 @@
debugLevel: debug_level,
protectedVm: protected,
memoryMib: mem.unwrap_or(0) as i32, // 0 means use the VM default
- numCpus: cpus.unwrap_or(1) as i32,
+ cpuTopology: cpu_topology,
taskProfiles: task_profiles,
+ gdbPort: gdb.map(u16::from).unwrap_or(0) as i32, // 0 means no gdb
});
run(service, &config, &payload_config_str, console_path, log_path)
}
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 {
@@ -182,8 +186,9 @@
debug_level: DebugLevel,
protected: bool,
mem: Option<u32>,
- cpus: Option<u32>,
+ cpu_topology: CpuTopology,
task_profiles: Vec<String>,
+ gdb: Option<NonZeroU16>,
) -> Result<(), Error> {
let apk = find_empty_payload_apk_path()?;
println!("found path {}", apk.display());
@@ -211,9 +216,10 @@
debug_level,
protected,
mem,
- cpus,
+ cpu_topology,
task_profiles,
&extra_sig,
+ gdb,
)
}
@@ -226,8 +232,9 @@
console_path: Option<&Path>,
log_path: Option<&Path>,
mem: Option<u32>,
- cpus: Option<u32>,
+ cpu_topology: CpuTopology,
task_profiles: Vec<String>,
+ gdb: Option<NonZeroU16>,
) -> Result<(), Error> {
let config_file = File::open(config_path).context("Failed to open config file")?;
let mut config =
@@ -235,14 +242,15 @@
if let Some(mem) = mem {
config.memoryMib = mem as i32;
}
- if let Some(cpus) = cpus {
- config.numCpus = cpus as i32;
- }
if let Some(name) = name {
config.name = name;
} else {
config.name = String::from("VmRun");
}
+ if let Some(gdb) = gdb {
+ config.gdbPort = gdb.get() as i32;
+ }
+ config.cpuTopology = cpu_topology;
config.taskProfiles = task_profiles;
run(
service,
diff --git a/vmbase/entry.S b/vmbase/entry.S
index ab46465..408f5d1 100644
--- a/vmbase/entry.S
+++ b/vmbase/entry.S
@@ -225,6 +225,9 @@
adr_l x30, __stack_chk_guard
str x29, [x30]
+ /* Write a null byte to the top of the stack guard to act as a string terminator. */
+ strb wzr, [x30]
+
/* Call into Rust code. */
bl rust_entry
diff --git a/vmbase/example/tests/test.rs b/vmbase/example/tests/test.rs
index c6aea8c..930e137 100644
--- a/vmbase/example/tests/test.rs
+++ b/vmbase/example/tests/test.rs
@@ -16,7 +16,7 @@
use android_system_virtualizationservice::{
aidl::android::system::virtualizationservice::{
- DiskImage::DiskImage, VirtualMachineConfig::VirtualMachineConfig,
+ CpuTopology::CpuTopology, DiskImage::DiskImage, VirtualMachineConfig::VirtualMachineConfig,
VirtualMachineRawConfig::VirtualMachineRawConfig,
},
binder::{ParcelFileDescriptor, ProcessState},
@@ -84,9 +84,10 @@
disks: vec![disk_image],
protectedVm: false,
memoryMib: 300,
- numCpus: 1,
+ cpuTopology: CpuTopology::ONE_CPU,
platformVersion: "~1.0".to_string(),
taskProfiles: vec![],
+ gdbPort: 0, // no gdb
});
let console = android_log_fd()?;
let log = android_log_fd()?;
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..d67d87e 100644
--- a/vmclient/src/lib.rs
+++ b/vmclient/src/lib.rs
@@ -57,7 +57,7 @@
"android.system.virtualizationservice";
const VIRTMGR_PATH: &str = "/apex/com.android.virt/bin/virtmgr";
-const VIRTMGR_THREADS: usize = 16;
+const VIRTMGR_THREADS: usize = 2;
fn posix_pipe() -> Result<(OwnedFd, OwnedFd), io::Error> {
use nix::fcntl::OFlag;
@@ -122,7 +122,6 @@
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
.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 {