Merge "Stop exposing libdiced_open_dice_nostd to //system/authgraph/tests" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index db0b43a..3651dfa 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -66,6 +66,11 @@
"keywords": ["internal"]
}
],
+ "ferrochrome-postsubmit": [
+ {
+ "name": "ferrochrome-tests"
+ }
+ ],
"postsubmit": [
{
"name": "CtsMicrodroidDisabledTestCases"
diff --git a/apex/Android.bp b/apex/Android.bp
index 43819dc..17b1f9e 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -157,6 +157,7 @@
"microdroid.json",
"microdroid_kernel",
"com.android.virt.init.rc",
+ "android_bootloader_crosvm_aarch64",
],
host_required: [
"vm_shell",
diff --git a/apex/product_packages.mk b/apex/product_packages.mk
index a318817..486334c 100644
--- a/apex/product_packages.mk
+++ b/apex/product_packages.mk
@@ -56,3 +56,9 @@
$(error RELEASE_AVF_ENABLE_DICE_CHANGES must also be enabled)
endif
endif
+
+ifdef RELEASE_AVF_ENABLE_NETWORK
+ ifndef RELEASE_AVF_ENABLE_LLPVM_CHANGES
+ $(error RELEASE_AVF_ENABLE_LLPVM_CHANGES must also be enabled)
+ endif
+endif
diff --git a/compos/Android.bp b/compos/Android.bp
index b840506..220533a 100644
--- a/compos/Android.bp
+++ b/compos/Android.bp
@@ -25,7 +25,7 @@
"librpcbinder_rs",
"librustutils",
"libscopeguard",
- "libvm_payload_bindgen",
+ "libvm_payload_rs",
],
prefer_rlib: true,
shared_libs: [
diff --git a/compos/src/compsvc_main.rs b/compos/src/compsvc_main.rs
index 06cc599..9bc522c 100644
--- a/compos/src/compsvc_main.rs
+++ b/compos/src/compsvc_main.rs
@@ -23,13 +23,9 @@
mod fsverity;
use anyhow::Result;
-use binder::unstable_api::AsNative;
use compos_common::COMPOS_VSOCK_PORT;
use log::{debug, error};
-use std::os::raw::c_void;
use std::panic;
-use std::ptr;
-use vm_payload_bindgen::{AIBinder, AVmPayload_notifyPayloadReady, AVmPayload_runVsockRpcServer};
fn main() {
if let Err(e) = try_main() {
@@ -50,17 +46,5 @@
}));
debug!("compsvc is starting as a rpc service.");
- let param = ptr::null_mut();
- let mut service = compsvc::new_binder()?.as_binder();
- let service = service.as_native_mut() as *mut AIBinder;
- // SAFETY: We hold a strong pointer, so the raw pointer remains valid. The bindgen AIBinder
- // is the same type as sys::AIBinder. It is safe for on_ready to be invoked at any time, with
- // any parameter.
- unsafe { AVmPayload_runVsockRpcServer(service, COMPOS_VSOCK_PORT, Some(on_ready), param) }
-}
-
-extern "C" fn on_ready(_param: *mut c_void) {
- // SAFETY: Invokes a method from the bindgen library `vm_payload_bindgen` which is safe to
- // call at any time.
- unsafe { AVmPayload_notifyPayloadReady() };
+ vm_payload::run_single_vsock_service(compsvc::new_binder()?, COMPOS_VSOCK_PORT)
}
diff --git a/compos/tests/java/android/compos/test/ComposTestCase.java b/compos/tests/java/android/compos/test/ComposTestCase.java
index bd011fa..b70e367 100644
--- a/compos/tests/java/android/compos/test/ComposTestCase.java
+++ b/compos/tests/java/android/compos/test/ComposTestCase.java
@@ -24,11 +24,13 @@
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import android.platform.test.annotations.RootPermissionTest;
import com.android.microdroid.test.host.CommandRunner;
import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
+import com.android.tradefed.device.TestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.LogDataType;
@@ -85,6 +87,8 @@
assumeDeviceIsCapable(getDevice());
// Test takes too long to run on Cuttlefish (b/292824951).
assumeFalse("Skipping test on Cuttlefish", isCuttlefish());
+ // CompOS requires a protected VM
+ assumeTrue(((TestDevice) getDevice()).supportsMicrodroid(/*protectedVm*/ true));
String value = getDevice().getProperty(SYSTEM_SERVER_COMPILER_FILTER_PROP_NAME);
if (value == null) {
@@ -193,6 +197,7 @@
10000,
validator.getAbsolutePath(),
"dice-chain",
+ "--allow-any-mode",
bcc_file.getAbsolutePath());
assertWithMessage("hwtrust failed").about(command_results()).that(result).isSuccess();
}
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index d52aa95..fce6da2 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -63,27 +63,30 @@
As of today (April 2024), ChromiumOS is the only officially supported guest
payload. We will be adding more OSes in the future.
-#### Download from build server
+#### Download ChromiumOS from build server
- - Step 1) Go to the link https://ci.chromium.org/ui/p/chromeos/builders/chromiumos/ferrochrome-public-main/
- - Note: I 'searched' the ferrochrome target with builder search.
- - Step 2) Click a build number
- - Step 3) Expand steps and find `48. upload artifacts`.
- - Step 4) Click `gs upload dir`. You'll see Cloud storage with comprehensive artifacts (e.g. [Here](https://pantheon.corp.google.com/storage/browser/chromiumos-image-archive/ferrochrome-public/R126-15883.0.0) is the initial build of ferrochrome)
- - Step 5) Download `image.zip`, which contains working vmlinuz.
- - Note: DO NOT DOWNLOAD `vmlinuz.tar.xz` from the CI.
- - Step 6) Uncompress `image.zip`, and boot with `chromiumos_test_image.bin` and `boot_images/vmlinuz`.
- - Note: DO NOT USE `vmlinuz.bin`.
+Download
+https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public/R128-15926.0.0/chromiumos_test_image.tar.xz.
+The above will download ferrochrome test image with version `R128-15926.0.0`.
-IMPORTANT: DO NOT USE `vmlinuz.bin` for passing to crosvm. It doesn't pick-up the correct `init` process (picks `/init` instead of `/sbin/init`, and `cfg80211` keeps crashing (i.e. no network)
+To download latest version, use following code.
+```sh
+URL=https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public
+LATEST_VERSION=$(curl -s ${URL}/LATEST-main)
+curl -O ${URL}/${LATEST_VERSION}/chromiumos_test_image.tar.xz
+```
+
+To navigate build server artifacts,
+[install gsutil](https://cloud.google.com/storage/docs/gsutil_install).
+`gs://chromiumos-image-archive/ferrochrome-public` is the top level directory for ferrochrome build.
#### Build ChromiumOS for VM
First, check out source code from the ChromiumOS and Chromium projects.
+* Checking out Chromium: https://www.chromium.org/developers/how-tos/get-the-code/
* Checking out ChromiumOS: https://www.chromium.org/chromium-os/developer-library/guides/development/developer-guide/
-* Checking out Chromium: https://g3doc.corp.google.com/chrome/chromeos/system_services_team/dev_instructions/g3doc/setup_checkout.md?cl=head
Important: When you are at the step “Set up gclient args” in the Chromium checkout instruction, configure .gclient as follows.
@@ -95,9 +98,7 @@
"url": "https://chromium.googlesource.com/chromium/src.git",
"managed": False,
"custom_deps": {},
- "custom_vars": {
- "checkout_src_internal": True,
- },
+ "custom_vars": {},
},
]
target_os = ['chromeos']
@@ -162,10 +163,7 @@
Don’t forget to call `build-image` afterwards.
-You need two outputs:
-
-* ChromiumOS disk image: ~/chromiumos/src/build/images/ferrochrome/latest/chromiumos_test_image.bin
-* The kernel: ~/chromiumos/src/build/images/ferrochrome/latest/boot_images/vmlinuz
+You need ChromiumOS disk image: ~/chromiumos/src/build/images/ferrochrome/latest/chromiumos_test_image.bin
### Create a guest VM configuration
@@ -173,7 +171,6 @@
```
$ adb push ~/chromiumos/src/build/images/ferrochrome/latest/chromiumos_test_image.bin /data/local/tmp/
-$ adb push ~/chromiumos/out/build/ferrochrome/boot/vmlinuz /data/local/tmp/kernel
```
Create a VM config file as below.
@@ -182,7 +179,6 @@
$ cat > vm_config.json; adb push vm_config.json /data/local/tmp
{
"name": "cros",
- "kernel": "/data/local/tmp/kernel",
"disks": [
{
"image": "/data/local/tmp/chromiumos_test_image.bin",
@@ -190,6 +186,10 @@
"writable": true
}
],
+ "gpu": {
+ "backend": "virglrenderer",
+ "context_types": ["virgl2"]
+ },
"params": "root=/dev/vda3 rootwait noinitrd ro enforcing=0 cros_debug cros_secure",
"protected": false,
"cpu_topology": "match_host",
@@ -206,7 +206,7 @@
```
$ adb root
-$ adb shell pm enable com.android.virtualization.vmlauncher/.MainActivity
+$ adb shell pm enable com.android.virtualization.vmlauncher/.MainActivityAlias
$ adb unroot
```
@@ -214,77 +214,22 @@
permission to the app.
```
$ adb root
-$ adb shell pm enable com.google.android.virtualization.vmlauncher/com.android.virtualization.vmlauncher.MainActivity
+$ adb shell pm enable com.google.android.virtualization.vmlauncher/com.android.virtualization.vmlauncher.MainActivityAlias
$ adb shell pm grant com.google.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
$ adb unroot
```
-Then execute the below to set up the network. In the future, this step won't be necessary.
-```
-$ cat > setup_network.sh; adb push setup_network.sh /data/local/tmp
-#!/system/bin/sh
+Second, ensure your device is connected to the Internet.
-set -e
-
-TAP_IFACE=crosvm_tap
-TAP_ADDR=192.168.1.1
-TAP_NET=192.168.1.0
-
-function setup_network() {
- local WAN_IFACE=$(ip route get 8.8.8.8 2> /dev/null | awk -- '{printf $5}')
- if [ "${WAN_IFACE}" == "" ]; then
- echo "No network. Connect to a WiFi network and start again"
- return 1
- fi
-
- if ip link show ${TAP_IFACE} &> /dev/null ; then
- echo "TAP interface ${TAP_IFACE} already exists"
- return 1
- fi
-
- ip tuntap add mode tap group virtualmachine vnet_hdr ${TAP_IFACE}
- ip addr add ${TAP_ADDR}/24 dev ${TAP_IFACE}
- ip link set ${TAP_IFACE} up
- ip rule flush
- ip rule add from all lookup ${WAN_IFACE}
- ip route add ${TAP_NET}/24 dev ${TAP_IFACE} table ${WAN_IFACE}
- sysctl net.ipv4.ip_forward=1
- iptables -t filter -F
- iptables -t nat -A POSTROUTING -s ${TAP_NET}/24 -j MASQUERADE
-}
-
-function setup_if_necessary() {
- if [ "$(getprop ro.crosvm.network.setup.done)" == 1 ]; then
- return
- fi
- echo "Setting up..."
- check_privilege
- setup_network
- setenforce 0
- chmod 666 /dev/tun
- setprop ro.crosvm.network.setup.done 1
-}
-
-function check_privilege() {
- if [ "$(id -u)" -ne 0 ]; then
- echo "Run 'adb root' first"
- return 1
- fi
-}
-
-setup_if_necessary
-^D
-
-adb root; adb shell /data/local/tmp/setup_network.sh
-```
-
-Then, finally tap the VmLauncherApp app from the launcher UI. You will see
+Finally, tap the VmLauncherApp app from the launcher UI. You will see
Ferrochrome booting!
If it doesn’t work well, try
```
$ adb shell pm clear com.android.virtualization.vmlauncher
+# or
+$ adb shell pm clear com.google.android.virtualization.vmlauncher
```
### Inside guest OS (for ChromiumOS only)
@@ -301,8 +246,19 @@
### Debugging
-To see console log, check
+To open the serial console (interactive terminal):
+```shell
+$ adb shell -t /apex/com.android.virt/bin/vm console
+```
+
+To see console logs only, check
`/data/data/com.android.virtualization.vmlauncher/files/console.log`
+Or
+`/data/data/com.google.android.virtualization.vmlauncher/files/console.log`
+
+```shell
+$ adb shell su root tail +0 -F /data/data/com{,.google}.android.virtualization.vmlauncher/files/console.log
+```
For ChromiumOS, you can ssh-in. Use following commands after network setup.
diff --git a/ferrochrome_app/Android.bp b/ferrochrome_app/Android.bp
new file mode 100644
index 0000000..b3998a7
--- /dev/null
+++ b/ferrochrome_app/Android.bp
@@ -0,0 +1,26 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "FerrochromeApp",
+ srcs: ["java/**/*.java"],
+ resource_dirs: ["res"],
+ platform_apis: true,
+ // TODO(b/348113995): move this app to product partition
+ system_ext_specific: true,
+ privileged: true,
+ init_rc: ["custom_vm_setup.rc"],
+ required: ["custom_vm_setup"],
+ certificate: "platform",
+ static_libs: [
+ "apache-commons-compress",
+ ],
+}
+
+sh_binary {
+ name: "custom_vm_setup",
+ src: "custom_vm_setup.sh",
+ system_ext_specific: true,
+ host_supported: false,
+}
diff --git a/ferrochrome_app/AndroidManifest.xml b/ferrochrome_app/AndroidManifest.xml
new file mode 100644
index 0000000..d783bbc
--- /dev/null
+++ b/ferrochrome_app/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.virtualization.ferrochrome"
+ android:sharedUserId="android.uid.system" >
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <application
+ android:label="Ferrochrome">
+ <activity android:name=".FerrochromeActivity"
+ android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
+ android:theme="@style/MyTheme"
+ android:screenOrientation="landscape"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/ferrochrome_app/custom_vm_setup.rc b/ferrochrome_app/custom_vm_setup.rc
new file mode 100644
index 0000000..68f370e
--- /dev/null
+++ b/ferrochrome_app/custom_vm_setup.rc
@@ -0,0 +1,23 @@
+# Copyright (C) 2024 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.
+
+service custom_vm_setup /system_ext/bin/custom_vm_setup
+ user shell
+ group shell media_rw
+ disabled
+ oneshot
+ seclabel u:r:shell:s0
+
+on property:debug.custom_vm_setup.start=true
+ start custom_vm_setup
diff --git a/ferrochrome_app/custom_vm_setup.sh b/ferrochrome_app/custom_vm_setup.sh
new file mode 100644
index 0000000..f007f6a
--- /dev/null
+++ b/ferrochrome_app/custom_vm_setup.sh
@@ -0,0 +1,14 @@
+#!/system/bin/sh
+
+function copy_files() {
+ cp -u /sdcard/vm_config.json /data/local/tmp
+ cp -u /data/media/10/vm_config.json /data/local/tmp
+ cp -u /sdcard/chromiumos_test_image.bin /data/local/tmp
+ cp -u /data/media/10/chromiumos_test_image.bin /data/local/tmp
+ chmod 666 /data/local/tmp/vm_config.json
+ chmod 666 /data/local/tmp/chromiumos_test_image.bin
+}
+setprop debug.custom_vm_setup.done false
+copy_files
+setprop debug.custom_vm_setup.start false
+setprop debug.custom_vm_setup.done true
diff --git a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
new file mode 100644
index 0000000..d9e5229
--- /dev/null
+++ b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2024 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.virtualization.ferrochrome;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class FerrochromeActivity extends Activity {
+ ExecutorService executorService = Executors.newSingleThreadExecutor();
+ private static final String TAG = "FerrochromeActivity";
+ private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
+ private static final String FERROCHROME_VERSION = "R128-15926.0.0";
+ private static final String EXTERNAL_STORAGE_DIR =
+ Environment.getExternalStorageDirectory().getPath() + File.separator;
+ private static final Path IMAGE_PATH =
+ Path.of(EXTERNAL_STORAGE_DIR + "chromiumos_test_image.bin");
+ private static final Path IMAGE_VERSION_INFO =
+ Path.of(EXTERNAL_STORAGE_DIR + "ferrochrome_image_version");
+ private static final Path VM_CONFIG_PATH = Path.of(EXTERNAL_STORAGE_DIR + "vm_config.json");
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_ferrochrome);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ // Find VM Launcher
+ Intent intent = new Intent(ACTION_VM_LAUNCHER);
+ PackageManager pm = getPackageManager();
+ List<ResolveInfo> resolveInfos =
+ pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolveInfos == null || resolveInfos.size() != 1) {
+ updateStatus("Failed to resolve VM Launcher");
+ return;
+ }
+
+ // Clean up the existing vm launcher process if there is
+ ActivityManager am = getSystemService(ActivityManager.class);
+ am.killBackgroundProcesses(resolveInfos.get(0).activityInfo.packageName);
+
+ executorService.execute(
+ () -> {
+ if (Files.notExists(IMAGE_PATH)
+ || !FERROCHROME_VERSION.equals(getVersionInfo())) {
+ updateStatus("Starting first-time setup.");
+ updateStatus(
+ "Downloading Ferrochrome image. This can take about 5 to 10"
+ + " minutes, depending on your network speed.");
+ if (download(FERROCHROME_VERSION)) {
+ updateStatus("Done.");
+ } else {
+ updateStatus(
+ "Download failed. Check the internet connection and retry.");
+ return;
+ }
+ } else {
+ updateStatus("Ferrochrome is already downloaded.");
+ }
+ updateStatus("Updating VM config.");
+ copyVmConfigJson();
+ updateStatus("Updating VM images. This may take a few minutes.");
+ SystemProperties.set("debug.custom_vm_setup.start", "true");
+ while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
+ // Wait for custom_vm_setup
+ try {
+ Thread.sleep(1000);
+ } catch (Exception e) {
+ Log.d(TAG, e.toString());
+ }
+ }
+ updateStatus("Done.");
+ updateStatus("Starting Ferrochrome...");
+ runOnUiThread(() -> startActivity(intent));
+ });
+ }
+
+ private void updateStatus(String line) {
+ Log.d(TAG, line);
+ runOnUiThread(
+ () -> {
+ TextView statusView = findViewById(R.id.status_txt_view);
+ statusView.append(line + "\n");
+ });
+ }
+
+ private void copyVmConfigJson() {
+ try (InputStream is = getResources().openRawResource(R.raw.vm_config)) {
+ Files.copy(is, VM_CONFIG_PATH, StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException e) {
+ updateStatus(e.toString());
+ }
+ }
+
+ private String getVersionInfo() {
+ try {
+ return new String(Files.readAllBytes(IMAGE_VERSION_INFO), StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private boolean updateVersionInfo(String version) {
+ try {
+ Files.write(IMAGE_VERSION_INFO, version.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ Log.d(TAG, e.toString());
+ }
+ return true;
+ }
+
+ private boolean download(String version) {
+ String urlString =
+ "https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public/"
+ + version
+ + "/chromiumos_test_image.tar.xz";
+ try (InputStream is = (new URL(urlString)).openStream();
+ XZCompressorInputStream xz = new XZCompressorInputStream(is);
+ TarArchiveInputStream tar = new TarArchiveInputStream(xz)) {
+ TarArchiveEntry entry;
+ while ((entry = tar.getNextTarEntry()) != null) {
+ if (!entry.getName().contains("chromiumos_test_image.bin")) {
+ continue;
+ }
+ updateStatus("copy " + entry.getName() + " start");
+ Files.copy(tar, IMAGE_PATH, StandardCopyOption.REPLACE_EXISTING);
+ updateStatus("copy " + entry.getName() + " done");
+ updateVersionInfo(version);
+ break;
+ }
+ } catch (Exception e) {
+ updateStatus(e.toString());
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/ferrochrome_app/res/layout/activity_ferrochrome.xml b/ferrochrome_app/res/layout/activity_ferrochrome.xml
new file mode 100644
index 0000000..7d5e8aa
--- /dev/null
+++ b/ferrochrome_app/res/layout/activity_ferrochrome.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".FerrochromeActivity">
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/status_txt_view"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/ferrochrome_app/res/raw/vm_config.json b/ferrochrome_app/res/raw/vm_config.json
new file mode 100644
index 0000000..d79400c
--- /dev/null
+++ b/ferrochrome_app/res/raw/vm_config.json
@@ -0,0 +1,20 @@
+{
+ "name": "cros",
+ "disks": [
+ {
+ "image": "/data/local/tmp/chromiumos_test_image.bin",
+ "partitions": [],
+ "writable": true
+ }
+ ],
+ "params": "root=/dev/vda3 rootwait noinitrd ro enforcing=0 cros_debug cros_secure",
+ "protected": false,
+ "cpu_topology": "match_host",
+ "platform_version": "~1.0",
+ "memory_mib": 8096,
+ "gpu": {
+ "backend": "virglrenderer",
+ "context_types": ["virgl2"]
+ },
+ "console_input_device": "ttyS0"
+}
\ No newline at end of file
diff --git a/ferrochrome_app/res/values/themes.xml b/ferrochrome_app/res/values/themes.xml
new file mode 100644
index 0000000..c9a9ed2
--- /dev/null
+++ b/ferrochrome_app/res/values/themes.xml
@@ -0,0 +1,4 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <style name="MyTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
+ </style>
+</resources>
diff --git a/java/framework/Android.bp b/java/framework/Android.bp
index 26ea214..7c0240d 100644
--- a/java/framework/Android.bp
+++ b/java/framework/Android.bp
@@ -46,4 +46,9 @@
aconfig_declarations: [
"avf_aconfig_flags",
],
+ lint: {
+ warning_checks: [
+ "FlaggedApi",
+ ],
+ },
}
diff --git a/java/framework/api/test-current.txt b/java/framework/api/test-current.txt
index d20d543..7e8da26 100644
--- a/java/framework/api/test-current.txt
+++ b/java/framework/api/test-current.txt
@@ -31,6 +31,7 @@
field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_DICE_CHANGES = "com.android.kvm.DICE_CHANGES";
field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_LLPVM_CHANGES = "com.android.kvm.LLPVM_CHANGES";
field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_MULTI_TENANT = "com.android.kvm.MULTI_TENANT";
+ field public static final String FEATURE_NETWORK = "com.android.kvm.NETWORK";
field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_REMOTE_ATTESTATION = "com.android.kvm.REMOTE_ATTESTATION";
field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_VENDOR_MODULES = "com.android.kvm.VENDOR_MODULES";
}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index 43f3db0..bca36a4 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -924,6 +924,11 @@
}
rawConfig.inputDevices = inputDevices.toArray(new InputDevice[0]);
+ // Handle network support
+ if (vmConfig.getCustomImageConfig() != null) {
+ rawConfig.networkSupported = vmConfig.getCustomImageConfig().useNetwork();
+ }
+
return android.system.virtualizationservice.VirtualMachineConfig.rawConfig(rawConfig);
}
@@ -1132,7 +1137,9 @@
/**
* Runs this virtual machine. The returning of this method however doesn't mean that the VM has
* actually started running or the OS has booted there. Such events can be notified by
- * registering a callback using {@link #setCallback} before calling {@code run()}.
+ * registering a callback using {@link #setCallback} before calling {@code run()}. There is no
+ * limit other than available memory that limits the number of virtual machines that can run at
+ * the same time.
*
* <p>NOTE: This method may block and should not be called on the main thread.
*
@@ -1214,6 +1221,9 @@
service.createVm(vmConfigParcel, consoleOutFd, consoleInFd, mLogWriter);
mVirtualMachine.registerCallback(new CallbackTranslator(service));
mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
+ if (mConnectVmConsole) {
+ mVirtualMachine.setHostConsoleName(getHostConsoleName());
+ }
mVirtualMachine.start();
} catch (IOException e) {
throw new VirtualMachineException("failed to persist files", e);
@@ -1335,7 +1345,7 @@
* @hide
*/
@NonNull
- public String getHostConsoleName() throws VirtualMachineException {
+ private String getHostConsoleName() throws VirtualMachineException {
if (!mConnectVmConsole) {
throw new VirtualMachineException("Host console is not enabled");
}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index 4d3bf2d..e18aca2 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -77,9 +77,11 @@
private static final String TAG = "VirtualMachineConfig";
private static String[] EMPTY_STRING_ARRAY = {};
+ private static final String U_BOOT_PREBUILT_PATH = "/apex/com.android.virt/etc/u-boot.bin";
// These define the schema of the config file persisted on disk.
- private static final int VERSION = 8;
+ // Please bump up the version number when adding a new key.
+ private static final int VERSION = 10;
private static final String KEY_VERSION = "version";
private static final String KEY_PACKAGENAME = "packageName";
private static final String KEY_APKPATH = "apkPath";
@@ -98,6 +100,8 @@
private static final String KEY_VENDOR_DISK_IMAGE_PATH = "vendorDiskImagePath";
private static final String KEY_OS = "os";
private static final String KEY_EXTRA_APKS = "extraApks";
+ private static final String KEY_SHOULD_BOOST_UCLAMP = "shouldBoostUclamp";
+ private static final String KEY_SHOULD_USE_HUGEPAGES = "shouldUseHugepages";
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -205,6 +209,10 @@
/** OS name of the VM using payload binaries. */
@NonNull @OsName private final String mOs;
+ private final boolean mShouldBoostUclamp;
+
+ private final boolean mShouldUseHugepages;
+
@Retention(RetentionPolicy.SOURCE)
@StringDef(
prefix = "MICRODROID",
@@ -239,7 +247,9 @@
boolean vmConsoleInputSupported,
boolean connectVmConsole,
@Nullable File vendorDiskImage,
- @NonNull @OsName String os) {
+ @NonNull @OsName String os,
+ boolean shouldBoostUclamp,
+ boolean shouldUseHugepages) {
// This is only called from Builder.build(); the builder handles parameter validation.
mPackageName = packageName;
mApkPath = apkPath;
@@ -262,6 +272,8 @@
mConnectVmConsole = connectVmConsole;
mVendorDiskImage = vendorDiskImage;
mOs = os;
+ mShouldBoostUclamp = shouldBoostUclamp;
+ mShouldUseHugepages = shouldUseHugepages;
}
/** Loads a config from a file. */
@@ -362,6 +374,9 @@
}
}
+ builder.setShouldBoostUclamp(b.getBoolean(KEY_SHOULD_BOOST_UCLAMP));
+ builder.setShouldUseHugepages(b.getBoolean(KEY_SHOULD_USE_HUGEPAGES));
+
return builder.build();
}
@@ -412,6 +427,8 @@
String[] extraApks = mExtraApks.toArray(new String[0]);
b.putStringArray(KEY_EXTRA_APKS, extraApks);
}
+ b.putBoolean(KEY_SHOULD_BOOST_UCLAMP, mShouldBoostUclamp);
+ b.putBoolean(KEY_SHOULD_USE_HUGEPAGES, mShouldUseHugepages);
b.writeToStream(output);
}
@@ -653,6 +670,11 @@
Optional.ofNullable(customImageConfig.getBootloaderPath())
.map((path) -> openOrNull(new File(path), MODE_READ_ONLY))
.orElse(null);
+
+ if (config.kernel == null && config.bootloader == null) {
+ config.bootloader = openOrNull(new File(U_BOOT_PREBUILT_PATH), MODE_READ_ONLY);
+ }
+
config.params =
Optional.ofNullable(customImageConfig.getParams())
.map((params) -> TextUtils.join(" ", params))
@@ -677,6 +699,10 @@
Optional.ofNullable(customImageConfig.getDisplayConfig())
.map(dc -> dc.toParcelable())
.orElse(null);
+ config.gpuConfig =
+ Optional.ofNullable(customImageConfig.getGpuConfig())
+ .map(dc -> dc.toParcelable())
+ .orElse(null);
config.protectedVm = this.mProtectedVm;
config.memoryMib = bytesToMebiBytes(mMemoryBytes);
config.cpuTopology = (byte) this.mCpuTopology;
@@ -733,6 +759,7 @@
vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.ONE_CPU;
break;
}
+
if (mVendorDiskImage != null) {
VirtualMachineAppConfig.CustomConfig customConfig =
new VirtualMachineAppConfig.CustomConfig();
@@ -747,6 +774,10 @@
}
vsConfig.customConfig = customConfig;
}
+
+ vsConfig.boostUclamp = mShouldBoostUclamp;
+ vsConfig.hugePages = mShouldUseHugepages;
+
return vsConfig;
}
@@ -826,6 +857,8 @@
private boolean mConnectVmConsole = false;
@Nullable private File mVendorDiskImage;
@NonNull @OsName private String mOs = DEFAULT_OS;
+ private boolean mShouldBoostUclamp = false;
+ private boolean mShouldUseHugepages = false;
/**
* Creates a builder for the given context.
@@ -919,7 +952,9 @@
mVmConsoleInputSupported,
mConnectVmConsole,
mVendorDiskImage,
- mOs);
+ mOs,
+ mShouldBoostUclamp,
+ mShouldUseHugepages);
}
/**
@@ -1224,5 +1259,17 @@
mOs = requireNonNull(os, "os must not be null");
return this;
}
+
+ /** @hide */
+ public Builder setShouldBoostUclamp(boolean shouldBoostUclamp) {
+ mShouldBoostUclamp = shouldBoostUclamp;
+ return this;
+ }
+
+ /** @hide */
+ public Builder setShouldUseHugepages(boolean shouldUseHugepages) {
+ mShouldUseHugepages = shouldUseHugepages;
+ return this;
+ }
}
}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index 2fcad20..8d4886a 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -36,6 +36,8 @@
private static final String KEY_TOUCH = "touch";
private static final String KEY_KEYBOARD = "keyboard";
private static final String KEY_MOUSE = "mouse";
+ private static final String KEY_NETWORK = "network";
+ private static final String KEY_GPU = "gpu";
@Nullable private final String name;
@Nullable private final String kernelPath;
@@ -47,6 +49,8 @@
private final boolean touch;
private final boolean keyboard;
private final boolean mouse;
+ private final boolean network;
+ @Nullable private final GpuConfig gpuConfig;
@Nullable
public Disk[] getDisks() {
@@ -90,6 +94,10 @@
return mouse;
}
+ public boolean useNetwork() {
+ return network;
+ }
+
/** @hide */
public VirtualMachineCustomImageConfig(
String name,
@@ -101,7 +109,9 @@
DisplayConfig displayConfig,
boolean touch,
boolean keyboard,
- boolean mouse) {
+ boolean mouse,
+ boolean network,
+ GpuConfig gpuConfig) {
this.name = name;
this.kernelPath = kernelPath;
this.initrdPath = initrdPath;
@@ -112,6 +122,8 @@
this.touch = touch;
this.keyboard = keyboard;
this.mouse = mouse;
+ this.network = network;
+ this.gpuConfig = gpuConfig;
}
static VirtualMachineCustomImageConfig from(PersistableBundle customImageConfigBundle) {
@@ -142,6 +154,8 @@
builder.useTouch(customImageConfigBundle.getBoolean(KEY_TOUCH));
builder.useKeyboard(customImageConfigBundle.getBoolean(KEY_KEYBOARD));
builder.useMouse(customImageConfigBundle.getBoolean(KEY_MOUSE));
+ builder.useNetwork(customImageConfigBundle.getBoolean(KEY_NETWORK));
+ builder.setGpuConfig(GpuConfig.from(customImageConfigBundle.getPersistableBundle(KEY_GPU)));
return builder.build();
}
@@ -173,6 +187,10 @@
pb.putBoolean(KEY_TOUCH, touch);
pb.putBoolean(KEY_KEYBOARD, keyboard);
pb.putBoolean(KEY_MOUSE, mouse);
+ pb.putBoolean(KEY_NETWORK, network);
+ pb.putPersistableBundle(
+ KEY_GPU,
+ Optional.ofNullable(gpuConfig).map(gc -> gc.toPersistableBundle()).orElse(null));
return pb;
}
@@ -181,6 +199,11 @@
return displayConfig;
}
+ @Nullable
+ public GpuConfig getGpuConfig() {
+ return gpuConfig;
+ }
+
/** @hide */
public static final class Disk {
private final boolean writable;
@@ -224,6 +247,8 @@
private boolean touch;
private boolean keyboard;
private boolean mouse;
+ private boolean network;
+ private GpuConfig gpuConfig;
/** @hide */
public Builder() {}
@@ -271,6 +296,12 @@
}
/** @hide */
+ public Builder setGpuConfig(GpuConfig gpuConfig) {
+ this.gpuConfig = gpuConfig;
+ return this;
+ }
+
+ /** @hide */
public Builder useTouch(boolean touch) {
this.touch = touch;
return this;
@@ -289,6 +320,12 @@
}
/** @hide */
+ public Builder useNetwork(boolean network) {
+ this.network = network;
+ return this;
+ }
+
+ /** @hide */
public VirtualMachineCustomImageConfig build() {
return new VirtualMachineCustomImageConfig(
this.name,
@@ -300,7 +337,9 @@
displayConfig,
touch,
keyboard,
- mouse);
+ mouse,
+ network,
+ gpuConfig);
}
}
@@ -437,4 +476,223 @@
}
}
}
+
+ /** @hide */
+ public static final class GpuConfig {
+ private static final String KEY_BACKEND = "backend";
+ private static final String KEY_CONTEXT_TYPES = "context_types";
+ private static final String KEY_PCI_ADDRESS = "pci_address";
+ private static final String KEY_RENDERER_FEATURES = "renderer_features";
+ private static final String KEY_RENDERER_USE_EGL = "renderer_use_egl";
+ private static final String KEY_RENDERER_USE_GLES = "renderer_use_gles";
+ private static final String KEY_RENDERER_USE_GLX = "renderer_use_glx";
+ private static final String KEY_RENDERER_USE_SURFACELESS = "renderer_use_surfaceless";
+ private static final String KEY_RENDERER_USE_VULKAN = "renderer_use_vulkan";
+
+ private final String backend;
+ private final String[] contextTypes;
+ private final String pciAddress;
+ private final String rendererFeatures;
+ private final boolean rendererUseEgl;
+ private final boolean rendererUseGles;
+ private final boolean rendererUseGlx;
+ private final boolean rendererUseSurfaceless;
+ private final boolean rendererUseVulkan;
+
+ private GpuConfig(
+ String backend,
+ String[] contextTypes,
+ String pciAddress,
+ String rendererFeatures,
+ boolean rendererUseEgl,
+ boolean rendererUseGles,
+ boolean rendererUseGlx,
+ boolean rendererUseSurfaceless,
+ boolean rendererUseVulkan) {
+ this.backend = backend;
+ this.contextTypes = contextTypes;
+ this.pciAddress = pciAddress;
+ this.rendererFeatures = rendererFeatures;
+ this.rendererUseEgl = rendererUseEgl;
+ this.rendererUseGles = rendererUseGles;
+ this.rendererUseGlx = rendererUseGlx;
+ this.rendererUseSurfaceless = rendererUseSurfaceless;
+ this.rendererUseVulkan = rendererUseVulkan;
+ }
+
+ /** @hide */
+ public String getBackend() {
+ return backend;
+ }
+
+ /** @hide */
+ public String[] getContextTypes() {
+ return contextTypes;
+ }
+
+ /** @hide */
+ public String getPciAddress() {
+ return pciAddress;
+ }
+
+ /** @hide */
+ public String getRendererFeatures() {
+ return rendererFeatures;
+ }
+
+ /** @hide */
+ public boolean getRendererUseEgl() {
+ return rendererUseEgl;
+ }
+
+ /** @hide */
+ public boolean getRendererUseGles() {
+ return rendererUseGles;
+ }
+
+ /** @hide */
+ public boolean getRendererUseGlx() {
+ return rendererUseGlx;
+ }
+
+ /** @hide */
+ public boolean getRendererUseSurfaceless() {
+ return rendererUseSurfaceless;
+ }
+
+ /** @hide */
+ public boolean getRendererUseVulkan() {
+ return rendererUseVulkan;
+ }
+
+ android.system.virtualizationservice.GpuConfig toParcelable() {
+ android.system.virtualizationservice.GpuConfig parcelable =
+ new android.system.virtualizationservice.GpuConfig();
+ parcelable.backend = this.backend;
+ parcelable.contextTypes = this.contextTypes;
+ parcelable.pciAddress = this.pciAddress;
+ parcelable.rendererFeatures = this.rendererFeatures;
+ parcelable.rendererUseEgl = this.rendererUseEgl;
+ parcelable.rendererUseGles = this.rendererUseGles;
+ parcelable.rendererUseGlx = this.rendererUseGlx;
+ parcelable.rendererUseSurfaceless = this.rendererUseSurfaceless;
+ parcelable.rendererUseVulkan = this.rendererUseVulkan;
+ return parcelable;
+ }
+
+ private static GpuConfig from(PersistableBundle pb) {
+ if (pb == null) {
+ return null;
+ }
+ Builder builder = new Builder();
+ builder.setBackend(pb.getString(KEY_BACKEND));
+ builder.setContextTypes(pb.getStringArray(KEY_CONTEXT_TYPES));
+ builder.setPciAddress(pb.getString(KEY_PCI_ADDRESS));
+ builder.setRendererFeatures(pb.getString(KEY_RENDERER_FEATURES));
+ builder.setRendererUseEgl(pb.getBoolean(KEY_RENDERER_USE_EGL));
+ builder.setRendererUseGles(pb.getBoolean(KEY_RENDERER_USE_GLES));
+ builder.setRendererUseGlx(pb.getBoolean(KEY_RENDERER_USE_GLX));
+ builder.setRendererUseSurfaceless(pb.getBoolean(KEY_RENDERER_USE_SURFACELESS));
+ builder.setRendererUseVulkan(pb.getBoolean(KEY_RENDERER_USE_VULKAN));
+ return builder.build();
+ }
+
+ private PersistableBundle toPersistableBundle() {
+ PersistableBundle pb = new PersistableBundle();
+ pb.putString(KEY_BACKEND, this.backend);
+ pb.putStringArray(KEY_CONTEXT_TYPES, this.contextTypes);
+ pb.putString(KEY_PCI_ADDRESS, this.pciAddress);
+ pb.putString(KEY_RENDERER_FEATURES, this.rendererFeatures);
+ pb.putBoolean(KEY_RENDERER_USE_EGL, this.rendererUseEgl);
+ pb.putBoolean(KEY_RENDERER_USE_GLES, this.rendererUseGles);
+ pb.putBoolean(KEY_RENDERER_USE_GLX, this.rendererUseGlx);
+ pb.putBoolean(KEY_RENDERER_USE_SURFACELESS, this.rendererUseSurfaceless);
+ pb.putBoolean(KEY_RENDERER_USE_VULKAN, this.rendererUseVulkan);
+ return pb;
+ }
+
+ /** @hide */
+ public static class Builder {
+ private String backend;
+ private String[] contextTypes;
+ private String pciAddress;
+ private String rendererFeatures;
+ private boolean rendererUseEgl = true;
+ private boolean rendererUseGles = true;
+ private boolean rendererUseGlx = false;
+ private boolean rendererUseSurfaceless = true;
+ private boolean rendererUseVulkan = false;
+
+ /** @hide */
+ public Builder() {}
+
+ /** @hide */
+ public Builder setBackend(String backend) {
+ this.backend = backend;
+ return this;
+ }
+
+ /** @hide */
+ public Builder setContextTypes(String[] contextTypes) {
+ this.contextTypes = contextTypes;
+ return this;
+ }
+
+ /** @hide */
+ public Builder setPciAddress(String pciAddress) {
+ this.pciAddress = pciAddress;
+ return this;
+ }
+
+ /** @hide */
+ public Builder setRendererFeatures(String rendererFeatures) {
+ this.rendererFeatures = rendererFeatures;
+ return this;
+ }
+
+ /** @hide */
+ public Builder setRendererUseEgl(Boolean rendererUseEgl) {
+ this.rendererUseEgl = rendererUseEgl;
+ return this;
+ }
+
+ /** @hide */
+ public Builder setRendererUseGles(Boolean rendererUseGles) {
+ this.rendererUseGles = rendererUseGles;
+ return this;
+ }
+
+ /** @hide */
+ public Builder setRendererUseGlx(Boolean rendererUseGlx) {
+ this.rendererUseGlx = rendererUseGlx;
+ return this;
+ }
+
+ /** @hide */
+ public Builder setRendererUseSurfaceless(Boolean rendererUseSurfaceless) {
+ this.rendererUseSurfaceless = rendererUseSurfaceless;
+ return this;
+ }
+
+ /** @hide */
+ public Builder setRendererUseVulkan(Boolean rendererUseVulkan) {
+ this.rendererUseVulkan = rendererUseVulkan;
+ return this;
+ }
+
+ /** @hide */
+ public GpuConfig build() {
+ return new GpuConfig(
+ backend,
+ contextTypes,
+ pciAddress,
+ rendererFeatures,
+ rendererUseEgl,
+ rendererUseGles,
+ rendererUseGlx,
+ rendererUseSurfaceless,
+ rendererUseVulkan);
+ }
+ }
+ }
}
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
index 4a9e943..abb2c81 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -123,6 +123,7 @@
FEATURE_DICE_CHANGES,
FEATURE_LLPVM_CHANGES,
FEATURE_MULTI_TENANT,
+ FEATURE_NETWORK,
FEATURE_REMOTE_ATTESTATION,
FEATURE_VENDOR_MODULES,
})
@@ -147,6 +148,13 @@
public static final String FEATURE_MULTI_TENANT = IVirtualizationService.FEATURE_MULTI_TENANT;
/**
+ * Feature to allow network features in VM.
+ *
+ * @hide
+ */
+ @TestApi public static final String FEATURE_NETWORK = IVirtualizationService.FEATURE_NETWORK;
+
+ /**
* Feature to allow remote attestation in Microdroid.
*
* @hide
diff --git a/java/service/Android.bp b/java/service/Android.bp
index 8bac7be..814445c 100644
--- a/java/service/Android.bp
+++ b/java/service/Android.bp
@@ -31,6 +31,7 @@
],
static_libs: [
"android.system.virtualizationmaintenance-java",
+ "android.system.vmtethering-java",
],
sdk_version: "core_platform",
apex_available: ["com.android.virt"],
diff --git a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
index 2461755..241eef4 100644
--- a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
+++ b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
@@ -21,16 +21,23 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.net.TetheringManager;
+import android.net.TetheringManager.StartTetheringCallback;
+import android.net.TetheringManager.TetheringRequest;
import android.os.Handler;
import android.os.IBinder;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.system.virtualizationmaintenance.IVirtualizationMaintenance;
+import android.system.vmtethering.IVmTethering;
import android.util.Log;
import com.android.internal.os.BackgroundThread;
import com.android.server.SystemService;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
/**
* This class exists to notify virtualization service of relevant things happening in the Android
* framework.
@@ -40,17 +47,25 @@
*/
public class VirtualizationSystemService extends SystemService {
private static final String TAG = VirtualizationSystemService.class.getName();
- private static final String SERVICE_NAME = "android.system.virtualizationmaintenance";
+ private static final String MAINTENANCE_SERVICE_NAME =
+ "android.system.virtualizationmaintenance";
private Handler mHandler;
+ private final TetheringService mTetheringService;
public VirtualizationSystemService(Context context) {
super(context);
+ if (Files.exists(Paths.get("/apex/com.android.virt/bin/vmnic"))) {
+ mTetheringService = new TetheringService();
+ } else {
+ mTetheringService = null;
+ }
}
@Override
public void onStart() {
- // Nothing needed here - we don't expose any binder service. The binder service we use is
- // exposed as a lazy service by the virtualizationservice native binary.
+ if (mTetheringService != null) {
+ publishBinderService(IVmTethering.DESCRIPTOR, mTetheringService);
+ }
}
@Override
@@ -82,11 +97,11 @@
}
static IVirtualizationMaintenance connectToMaintenanceService() {
- IBinder binder = ServiceManager.waitForService(SERVICE_NAME);
+ IBinder binder = ServiceManager.waitForService(MAINTENANCE_SERVICE_NAME);
IVirtualizationMaintenance maintenance =
IVirtualizationMaintenance.Stub.asInterface(binder);
if (maintenance == null) {
- throw new IllegalStateException("Failed to connect to " + SERVICE_NAME);
+ throw new IllegalStateException("Failed to connect to " + MAINTENANCE_SERVICE_NAME);
}
return maintenance;
}
@@ -136,4 +151,38 @@
}
}
}
+
+ private final class TetheringService extends IVmTethering.Stub {
+ private final TetheringManager tm = getContext().getSystemService(TetheringManager.class);
+
+ @Override
+ public void enableVmTethering() {
+ final TetheringRequest tr =
+ new TetheringRequest.Builder(TetheringManager.TETHERING_VIRTUAL)
+ .setConnectivityScope(TetheringManager.CONNECTIVITY_SCOPE_GLOBAL)
+ .build();
+
+ StartTetheringCallback startTetheringCallback =
+ new StartTetheringCallback() {
+ @Override
+ public void onTetheringStarted() {
+ Log.i(TAG, "VM tethering started successfully");
+ }
+
+ @Override
+ public void onTetheringFailed(int resultCode) {
+ Log.e(
+ TAG,
+ "VM tethering failed. Result Code: "
+ + Integer.toString(resultCode));
+ }
+ };
+ tm.startTethering(tr, c -> c.run() /* executor */, startTetheringCallback);
+ }
+
+ @Override
+ public void disableVmTethering() {
+ tm.stopTethering(TetheringManager.TETHERING_VIRTUAL);
+ }
+ }
}
diff --git a/libs/android_display_backend/crosvm_android_display_client.cpp b/libs/android_display_backend/crosvm_android_display_client.cpp
index 0557127..6e4a793 100644
--- a/libs/android_display_backend/crosvm_android_display_client.cpp
+++ b/libs/android_display_backend/crosvm_android_display_client.cpp
@@ -30,47 +30,179 @@
namespace {
+class SinkANativeWindow_Buffer {
+public:
+ SinkANativeWindow_Buffer() = default;
+ virtual ~SinkANativeWindow_Buffer() = default;
+
+ bool configure(uint32_t width, uint32_t height, int format) {
+ if (format != HAL_PIXEL_FORMAT_BGRA_8888) {
+ return false;
+ }
+
+ mBufferBits.resize(width * height * 4);
+ mBuffer = ANativeWindow_Buffer{
+ .width = static_cast<int32_t>(width),
+ .height = static_cast<int32_t>(height),
+ .stride = static_cast<int32_t>(width),
+ .format = format,
+ .bits = mBufferBits.data(),
+ };
+ return true;
+ }
+
+ operator ANativeWindow_Buffer&() { return mBuffer; }
+
+private:
+ ANativeWindow_Buffer mBuffer;
+ std::vector<uint8_t> mBufferBits;
+};
+
+// Wrapper which contains the latest available Surface/ANativeWindow
+// from the DisplayService, if available. A Surface/ANativeWindow may
+// not always be available if, for example, the VmLauncherApp on the
+// other end of the DisplayService is not in the foreground / is paused.
+class AndroidDisplaySurface {
+public:
+ AndroidDisplaySurface() = default;
+ virtual ~AndroidDisplaySurface() = default;
+
+ void setSurface(Surface* surface) {
+ {
+ std::lock_guard lk(mSurfaceMutex);
+ mNativeSurface = std::make_unique<Surface>(surface->release());
+ mNativeSurfaceNeedsConfiguring = true;
+ }
+
+ mNativeSurfaceReady.notify_one();
+ }
+
+ void removeSurface() {
+ {
+ std::lock_guard lk(mSurfaceMutex);
+ mNativeSurface = nullptr;
+ }
+ mNativeSurfaceReady.notify_one();
+ }
+
+ Surface* getSurface() {
+ std::unique_lock lk(mSurfaceMutex);
+ return mNativeSurface.get();
+ }
+
+ void configure(uint32_t width, uint32_t height) {
+ std::unique_lock lk(mSurfaceMutex);
+
+ mRequestedSurfaceDimensions = Rect{
+ .width = width,
+ .height = height,
+ };
+
+ mSinkBuffer.configure(width, height, kFormat);
+ }
+
+ void waitForNativeSurface() {
+ std::unique_lock lk(mSurfaceMutex);
+ mNativeSurfaceReady.wait(lk, [this] { return mNativeSurface != nullptr; });
+ }
+
+ int lock(ANativeWindow_Buffer* out_buffer) {
+ std::unique_lock lk(mSurfaceMutex);
+
+ Surface* surface = mNativeSurface.get();
+ if (surface == nullptr) {
+ // Surface not currently available but not necessarily an error
+ // if, for example, the VmLauncherApp is not in the foreground.
+ *out_buffer = mSinkBuffer;
+ return 0;
+ }
+
+ ANativeWindow* anw = surface->get();
+ if (anw == nullptr) {
+ return -1;
+ }
+
+ if (mNativeSurfaceNeedsConfiguring) {
+ if (!mRequestedSurfaceDimensions) {
+ return -1;
+ }
+ const auto& dims = *mRequestedSurfaceDimensions;
+
+ // Ensure locked buffers have our desired format.
+ if (ANativeWindow_setBuffersGeometry(anw, dims.width, dims.height, kFormat) != 0) {
+ return -1;
+ }
+
+ mNativeSurfaceNeedsConfiguring = false;
+ }
+
+ return ANativeWindow_lock(anw, out_buffer, nullptr);
+ }
+
+ int unlockAndPost() {
+ std::unique_lock lk(mSurfaceMutex);
+
+ Surface* surface = mNativeSurface.get();
+ if (surface == nullptr) {
+ // Surface not currently available but not necessarily an error
+ // if, for example, the VmLauncherApp is not in the foreground.
+ return 0;
+ }
+
+ ANativeWindow* anw = surface->get();
+ if (anw == nullptr) {
+ return -1;
+ }
+
+ return ANativeWindow_unlockAndPost(anw);
+ }
+
+private:
+ // Note: crosvm always uses BGRA8888 or BGRX8888. See devices/src/virtio/gpu/mod.rs in
+ // crosvm where the SetScanoutBlob command is handled. Let's use BGRA not BGRX with a hope
+ // that we will need alpha blending for the cursor surface.
+ static constexpr const int kFormat = HAL_PIXEL_FORMAT_BGRA_8888;
+
+ std::mutex mSurfaceMutex;
+ std::unique_ptr<Surface> mNativeSurface;
+ std::condition_variable mNativeSurfaceReady;
+ bool mNativeSurfaceNeedsConfiguring = true;
+
+ SinkANativeWindow_Buffer mSinkBuffer;
+
+ struct Rect {
+ uint32_t width = 0;
+ uint32_t height = 0;
+ };
+ std::optional<Rect> mRequestedSurfaceDimensions;
+};
+
class DisplayService : public BnCrosvmAndroidDisplayService {
public:
DisplayService() = default;
virtual ~DisplayService() = default;
ndk::ScopedAStatus setSurface(Surface* surface, bool forCursor) override {
- {
- std::lock_guard lk(mSurfaceReadyMutex);
- if (forCursor) {
- mCursorSurface = std::make_unique<Surface>(surface->release());
- } else {
- mSurface = std::make_unique<Surface>(surface->release());
- }
+ if (forCursor) {
+ mCursor.setSurface(surface);
+ } else {
+ mScanout.setSurface(surface);
}
- mSurfaceReady.notify_all();
return ::ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus removeSurface(bool forCursor) override {
- {
- std::lock_guard lk(mSurfaceReadyMutex);
- if (forCursor) {
- mCursorSurface = nullptr;
- } else {
- mSurface = nullptr;
- }
+ if (forCursor) {
+ mCursor.removeSurface();
+ } else {
+ mScanout.removeSurface();
}
- mSurfaceReady.notify_all();
return ::ndk::ScopedAStatus::ok();
}
- Surface* getSurface(bool forCursor) {
- std::unique_lock lk(mSurfaceReadyMutex);
- if (forCursor) {
- mSurfaceReady.wait(lk, [this] { return mCursorSurface != nullptr; });
- return mCursorSurface.get();
- } else {
- mSurfaceReady.wait(lk, [this] { return mSurface != nullptr; });
- return mSurface.get();
- }
- }
+ AndroidDisplaySurface* getCursorSurface() { return &mCursor; }
+ AndroidDisplaySurface* getScanoutSurface() { return &mScanout; }
+
ndk::ScopedFileDescriptor& getCursorStream() { return mCursorStream; }
ndk::ScopedAStatus setCursorStream(const ndk::ScopedFileDescriptor& in_stream) {
mCursorStream = ndk::ScopedFileDescriptor(dup(in_stream.get()));
@@ -78,10 +210,8 @@
}
private:
- std::condition_variable mSurfaceReady;
- std::mutex mSurfaceReadyMutex;
- std::unique_ptr<Surface> mSurface;
- std::unique_ptr<Surface> mCursorSurface;
+ AndroidDisplaySurface mScanout;
+ AndroidDisplaySurface mCursor;
ndk::ScopedFileDescriptor mCursorStream;
};
@@ -149,25 +279,29 @@
delete ctx;
}
-extern "C" ANativeWindow* create_android_surface(struct AndroidDisplayContext* ctx, uint32_t width,
- uint32_t height, bool for_cursor) {
+extern "C" AndroidDisplaySurface* create_android_surface(struct AndroidDisplayContext* ctx,
+ uint32_t width, uint32_t height,
+ bool forCursor) {
if (ctx->disp_service == nullptr) {
ctx->errorf("Display service was not created");
return nullptr;
}
- // Note: crosvm always uses BGRA8888 or BGRX8888. See devices/src/virtio/gpu/mod.rs in crosvm
- // where the SetScanoutBlob command is handled. Let's use BGRA not BGRX with a hope that we will
- // need alpha blending for the cursor surface.
- int format = HAL_PIXEL_FORMAT_BGRA_8888;
- ANativeWindow* surface = ctx->disp_service->getSurface(for_cursor)->get(); // this can block
- if (ANativeWindow_setBuffersGeometry(surface, width, height, format) != 0) {
- ctx->errorf("Failed to set buffer gemoetry");
+
+ AndroidDisplaySurface* displaySurface = forCursor ? ctx->disp_service->getCursorSurface()
+ : ctx->disp_service->getScanoutSurface();
+ if (displaySurface == nullptr) {
+ ctx->errorf("AndroidDisplaySurface was not created");
return nullptr;
}
+
+ displaySurface->configure(width, height);
+
+ displaySurface->waitForNativeSurface(); // this can block
+
// TODO(b/332785161): if we know that surface can get destroyed dynamically while VM is running,
// consider calling ANativeWindow_acquire here and _release in destroy_android_surface, so that
// crosvm doesn't hold a dangling pointer.
- return surface;
+ return displaySurface;
}
extern "C" void destroy_android_surface(struct AndroidDisplayContext*, ANativeWindow*) {
@@ -175,16 +309,23 @@
}
extern "C" bool get_android_surface_buffer(struct AndroidDisplayContext* ctx,
- ANativeWindow* surface,
+ AndroidDisplaySurface* surface,
ANativeWindow_Buffer* out_buffer) {
if (out_buffer == nullptr) {
ctx->errorf("out_buffer is null");
return false;
}
- if (ANativeWindow_lock(surface, out_buffer, nullptr) != 0) {
+
+ if (surface == nullptr) {
+ ctx->errorf("Invalid AndroidDisplaySurface provided");
+ return false;
+ }
+
+ if (surface->lock(out_buffer) != 0) {
ctx->errorf("Failed to lock buffer");
return false;
}
+
return true;
}
@@ -204,9 +345,14 @@
}
extern "C" void post_android_surface_buffer(struct AndroidDisplayContext* ctx,
- ANativeWindow* surface) {
- if (ANativeWindow_unlockAndPost(surface) != 0) {
- ctx->errorf("Failed to unlock and post surface.");
+ AndroidDisplaySurface* surface) {
+ if (surface == nullptr) {
+ ctx->errorf("Invalid AndroidDisplaySurface provided");
+ return;
+ }
+
+ if (surface->unlockAndPost() != 0) {
+ ctx->errorf("Failed to unlock and post AndroidDisplaySurface.");
return;
}
}
diff --git a/libs/avf_features/src/lib.rs b/libs/avf_features/src/lib.rs
index c0faab0..1ebe2a4 100644
--- a/libs/avf_features/src/lib.rs
+++ b/libs/avf_features/src/lib.rs
@@ -16,7 +16,7 @@
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
IVirtualizationService::FEATURE_DICE_CHANGES, IVirtualizationService::FEATURE_LLPVM_CHANGES,
- IVirtualizationService::FEATURE_MULTI_TENANT,
+ IVirtualizationService::FEATURE_MULTI_TENANT, IVirtualizationService::FEATURE_NETWORK,
IVirtualizationService::FEATURE_REMOTE_ATTESTATION,
IVirtualizationService::FEATURE_VENDOR_MODULES,
};
@@ -28,6 +28,7 @@
FEATURE_DICE_CHANGES => cfg!(dice_changes),
FEATURE_LLPVM_CHANGES => cfg!(llpvm_changes),
FEATURE_MULTI_TENANT => cfg!(multi_tenant),
+ FEATURE_NETWORK => cfg!(network),
FEATURE_REMOTE_ATTESTATION => cfg!(remote_attestation),
FEATURE_VENDOR_MODULES => cfg!(vendor_modules),
_ => {
diff --git a/libs/hypervisor_props/src/lib.rs b/libs/hypervisor_props/src/lib.rs
index 14614fd..6665bc5 100644
--- a/libs/hypervisor_props/src/lib.rs
+++ b/libs/hypervisor_props/src/lib.rs
@@ -37,3 +37,8 @@
pub fn version() -> Result<Option<String>> {
Ok(hypervisorproperties::hypervisor_version()?)
}
+
+/// Returns if the hypervisor is pKVM
+pub fn is_pkvm() -> Result<bool> {
+ Ok(version()?.unwrap_or_default().starts_with("kvm") && is_protected_vm_supported()?)
+}
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index ff17ed1..c6d6090 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -395,12 +395,9 @@
// python -c "import hashlib; print(hashlib.sha256(b'bootloader').hexdigest())"
bootloader_salt = "3b4a12881d11f33cff968a24d7c53723a8232cde9a8d91e29fdbd6a95ae6adf0"
-// Note that keys can be different for filesystem images even though we're using the same key
-// for microdroid. However, the key signing VBmeta should match with the pubkey embedded in
-// bootloader.
filegroup {
name: "microdroid_sign_key",
- srcs: [":avb_testkey_rsa4096"],
+ srcs: [":pvmfw_embedded_key"],
}
soong_config_module_type {
diff --git a/microdroid_manager/src/verify.rs b/microdroid_manager/src/verify.rs
index 65c32b0..84feb68 100644
--- a/microdroid_manager/src/verify.rs
+++ b/microdroid_manager/src/verify.rs
@@ -14,7 +14,7 @@
use crate::instance::{ApexData, ApkData, MicrodroidData};
use crate::payload::{get_apex_data_from_payload, to_metadata};
-use crate::{is_strict_boot, is_verified_boot, MicrodroidError};
+use crate::{is_strict_boot, MicrodroidError};
use anyhow::{anyhow, ensure, Context, Result};
use apkmanifest::get_manifest_info;
use apkverify::{extract_signed_data, verify, V4Signature};
@@ -130,11 +130,10 @@
// APEX payload.
let apex_data_from_payload = get_apex_data_from_payload(metadata)?;
- // Writing /apex/vm-payload-metadata is to verify that the payload isn't changed.
- // Skip writing it if the debug policy ignoring identity is on
- if is_verified_boot() {
- write_apex_payload_data(saved_data, &apex_data_from_payload)?;
- }
+ // To prevent a TOCTOU attack, we need to make sure that when apexd verifies & mounts the
+ // APEXes it sees the same ones that we just read - so we write the metadata we just collected
+ // to a file (that the host can't access) that apexd will then verify against. See b/199371341.
+ write_apex_payload_data(saved_data, &apex_data_from_payload)?;
if cfg!(not(dice_changes)) {
// Start apexd to activate APEXes
@@ -222,16 +221,17 @@
saved_apex_data == apex_data_from_payload,
MicrodroidError::PayloadChanged(String::from("APEXes have changed."))
);
- let apex_metadata = to_metadata(apex_data_from_payload);
- // Pass metadata(with public keys and root digests) to apexd so that it uses the passed
- // metadata instead of the default one (/dev/block/by-name/payload-metadata)
- OpenOptions::new()
- .create_new(true)
- .write(true)
- .open("/apex/vm-payload-metadata")
- .context("Failed to open /apex/vm-payload-metadata")
- .and_then(|f| write_metadata(&apex_metadata, f))?;
}
+ let apex_metadata = to_metadata(apex_data_from_payload);
+ // Pass metadata(with public keys and root digests) to apexd so that it uses the passed
+ // metadata instead of the default one (/dev/block/by-name/payload-metadata)
+ OpenOptions::new()
+ .create_new(true)
+ .write(true)
+ .open("/apex/vm-payload-metadata")
+ .context("Failed to open /apex/vm-payload-metadata")
+ .and_then(|f| write_metadata(&apex_metadata, f))?;
+
Ok(())
}
diff --git a/microdroid_manager/src/vm_secret.rs b/microdroid_manager/src/vm_secret.rs
index ec40b45..c16a45e 100644
--- a/microdroid_manager/src/vm_secret.rs
+++ b/microdroid_manager/src/vm_secret.rs
@@ -20,7 +20,7 @@
use secretkeeper_comm::data_types::request::Request;
use binder::{Strong};
use coset::{CoseKey, CborSerializable, CborOrdering};
-use dice_policy_builder::{CertIndex, ConstraintSpec, ConstraintType, policy_for_dice_chain, MissingAction, WILDCARD_FULL_ARRAY};
+use dice_policy_builder::{TargetEntry, ConstraintSpec, ConstraintType, policy_for_dice_chain, MissingAction, WILDCARD_FULL_ARRAY};
use diced_open_dice::{DiceArtifacts, OwnedDiceArtifacts};
use keystore2_crypto::ZVec;
use openssl::hkdf::hkdf;
@@ -45,9 +45,10 @@
const SUBCOMPONENT_DESCRIPTORS: i64 = -71002;
const SUBCOMPONENT_SECURITY_VERSION: i64 = 2;
const SUBCOMPONENT_AUTHORITY_HASH: i64 = 4;
-// Index of DiceChainEntry corresponding to Payload (relative to the end considering DICE Chain
-// as an array)
-const PAYLOAD_INDEX_FROM_END: usize = 0;
+// See dice_for_avf_guest.cddl for the `component_name` used by different boot stages in guest VM.
+const MICRODROID_PAYLOAD_COMPONENT_NAME: &str = "Microdroid payload";
+const GUEST_OS_COMPONENT_NAME: &str = "vm_entry";
+const INSTANCE_HASH_KEY: i64 = -71003;
// Generated using hexdump -vn32 -e'14/1 "0x%02X, " 1 "\n"' /dev/urandom
const SALT_ENCRYPTED_STORE: &[u8] = &[
@@ -173,25 +174,27 @@
// microdroid_manager/src/vm_config.cddl):
// - GreaterOrEqual on SECURITY_VERSION (Required)
// - ExactMatch on AUTHORITY_HASH (Required).
+// 5. ExactMatch on Instance Hash (Required) - This uniquely identifies one VM instance from
+// another even if they are running the exact same images.
fn sealing_policy(dice: &[u8]) -> Result<Vec<u8>, String> {
- let constraint_spec = [
+ let constraint_spec = vec![
ConstraintSpec::new(
ConstraintType::ExactMatch,
vec![AUTHORITY_HASH],
MissingAction::Fail,
- CertIndex::All,
+ TargetEntry::All,
),
ConstraintSpec::new(
ConstraintType::ExactMatch,
vec![MODE],
MissingAction::Fail,
- CertIndex::All,
+ TargetEntry::All,
),
ConstraintSpec::new(
ConstraintType::GreaterOrEqual,
vec![CONFIG_DESC, SECURITY_VERSION],
MissingAction::Ignore,
- CertIndex::All,
+ TargetEntry::All,
),
ConstraintSpec::new(
ConstraintType::GreaterOrEqual,
@@ -202,7 +205,7 @@
SUBCOMPONENT_SECURITY_VERSION,
],
MissingAction::Fail,
- CertIndex::FromEnd(PAYLOAD_INDEX_FROM_END),
+ TargetEntry::ByName(MICRODROID_PAYLOAD_COMPONENT_NAME.to_string()),
),
ConstraintSpec::new(
ConstraintType::ExactMatch,
@@ -213,11 +216,17 @@
SUBCOMPONENT_AUTHORITY_HASH,
],
MissingAction::Fail,
- CertIndex::FromEnd(PAYLOAD_INDEX_FROM_END),
+ TargetEntry::ByName(MICRODROID_PAYLOAD_COMPONENT_NAME.to_string()),
+ ),
+ ConstraintSpec::new(
+ ConstraintType::ExactMatch,
+ vec![CONFIG_DESC, INSTANCE_HASH_KEY],
+ MissingAction::Fail,
+ TargetEntry::ByName(GUEST_OS_COMPONENT_NAME.to_string()),
),
];
- policy_for_dice_chain(dice, &constraint_spec)?
+ policy_for_dice_chain(dice, constraint_spec)?
.to_vec()
.map_err(|e| format!("DicePolicy construction failed {e:?}"))
}
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index 0d57301..c2e45f2 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -71,7 +71,7 @@
check_processing_reverse_request(&mut vm)?;
let key_pair = check_processing_generating_key_pair_request(&mut vm)?;
- check_processing_generating_certificate_request(&mut vm, &key_pair.maced_public_key)?;
+ check_processing_generating_certificate_request(&mut vm, &key_pair.maced_public_key, vm_type)?;
check_attestation_request(&mut vm, &key_pair, vm_type)?;
Ok(())
}
@@ -111,6 +111,7 @@
fn check_processing_generating_certificate_request(
vm: &mut ServiceVm,
maced_public_key: &[u8],
+ vm_type: VmType,
) -> Result<()> {
let params = GenerateCertificateRequestParams {
keys_to_sign: vec![maced_public_key.to_vec()],
@@ -122,7 +123,7 @@
info!("Received response: {response:?}.");
match response {
- Response::GenerateCertificateRequest(csr) => check_csr(csr),
+ Response::GenerateCertificateRequest(csr) => check_csr(csr, vm_type),
_ => bail!("Incorrect response type: {response:?}"),
}
}
@@ -275,8 +276,14 @@
Ok(())
}
-fn check_csr(csr: Vec<u8>) -> Result<()> {
- let _csr = rkp::Csr::from_cbor(&Session::default(), &csr[..]).context("Failed to parse CSR")?;
+fn check_csr(csr: Vec<u8>, vm_type: VmType) -> Result<()> {
+ let mut session = Session::default();
+
+ // Allow any mode for non-protected VMs because they use a fake DICE chain with the mode set to
+ // debug.
+ session.set_allow_any_mode(vm_type == VmType::NonProtectedVm);
+
+ let _csr = rkp::Csr::from_cbor(&session, &csr[..]).context("Failed to parse CSR")?;
Ok(())
}
diff --git a/service_vm/demo_apk/Android.bp b/service_vm/demo_apk/Android.bp
index 3750fe6..c64b70a 100644
--- a/service_vm/demo_apk/Android.bp
+++ b/service_vm/demo_apk/Android.bp
@@ -23,7 +23,7 @@
"libandroid_logger",
"libanyhow",
"liblog_rust",
- "libvm_payload_bindgen",
+ "libvm_payload_rs",
],
}
diff --git a/service_vm/demo_apk/src/main.rs b/service_vm/demo_apk/src/main.rs
index 8ea4e65..26df52c 100644
--- a/service_vm/demo_apk/src/main.rs
+++ b/service_vm/demo_apk/src/main.rs
@@ -14,25 +14,15 @@
//! Main executable of Service VM client for manual testing.
-use anyhow::{anyhow, ensure, Result};
+use anyhow::{ensure, Context, Result};
use log::{error, info};
-use std::{
- ffi::{c_void, CStr},
- panic,
- ptr::{self, NonNull},
- result,
-};
-use vm_payload_bindgen::{
- AVmAttestationResult, AVmAttestationResult_free, AVmAttestationResult_getCertificateAt,
- AVmAttestationResult_getCertificateCount, AVmAttestationResult_getPrivateKey,
- AVmAttestationResult_sign, AVmAttestationStatus, AVmAttestationStatus_toString,
- AVmPayload_requestAttestation,
-};
+use std::panic;
+use vm_payload::AttestationError;
+
+vm_payload::main!(main);
/// Entry point of the Service VM client.
-#[allow(non_snake_case)]
-#[no_mangle]
-pub extern "C" fn AVmPayload_main() {
+fn main() {
android_logger::init_once(
android_logger::Config::default()
.with_tag("service_vm_client")
@@ -52,15 +42,11 @@
info!("Welcome to Service VM Client!");
let too_big_challenge = &[0u8; 66];
- let res = AttestationResult::request_attestation(too_big_challenge);
+ let res = vm_payload::request_attestation(too_big_challenge);
ensure!(res.is_err());
- let status = res.unwrap_err();
- ensure!(
- status == AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE,
- "Unexpected status: {:?}",
- status
- );
- info!("Status: {:?}", status_to_cstr(status));
+ let error = res.unwrap_err();
+ ensure!(error == AttestationError::InvalidChallenge, "Unexpected error: {error:?}");
+ info!("Error: {error}");
// The data below is only a placeholder generated randomly with urandom
let challenge = &[
@@ -68,162 +54,18 @@
0x67, 0xc3, 0x3e, 0x73, 0x9b, 0x30, 0xbd, 0x04, 0x20, 0x2e, 0xde, 0x3b, 0x1d, 0xc8, 0x07,
0x11, 0x7b,
];
- let res = AttestationResult::request_attestation(challenge)
- .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))?;
+ let res = vm_payload::request_attestation(challenge).context("Unexpected attestation error")?;
- let cert_chain = res.certificate_chain()?;
+ let cert_chain: Vec<_> = res.certificate_chain().collect();
info!("Attestation result certificateChain = {:?}", cert_chain);
- let private_key = res.private_key()?;
+ let private_key = res.private_key();
info!("Attestation result privateKey = {:?}", private_key);
let message = b"Hello from Service VM client";
info!("Signing message: {:?}", message);
- let signature = res.sign(message)?;
+ let signature = res.sign_message(message);
info!("Signature: {:?}", signature);
Ok(())
}
-
-#[derive(Debug)]
-struct AttestationResult(NonNull<AVmAttestationResult>);
-
-impl AttestationResult {
- fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
- let mut res: *mut AVmAttestationResult = ptr::null_mut();
- // SAFETY: It is safe as we only read the challenge within its bounds and the
- // function does not retain any reference to it.
- let status = unsafe {
- AVmPayload_requestAttestation(
- challenge.as_ptr() as *const c_void,
- challenge.len(),
- &mut res,
- )
- };
- if status == AVmAttestationStatus::ATTESTATION_OK {
- info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
- let res = NonNull::new(res).expect("The attestation result is null");
- Ok(Self(res))
- } else {
- Err(status)
- }
- }
-
- fn certificate_chain(&self) -> Result<Vec<Box<[u8]>>> {
- let num_certs = get_certificate_count(self.as_ref());
- let mut certs = Vec::with_capacity(num_certs);
- for i in 0..num_certs {
- certs.push(get_certificate_at(self.as_ref(), i)?);
- }
- Ok(certs)
- }
-
- fn private_key(&self) -> Result<Box<[u8]>> {
- get_private_key(self.as_ref())
- }
-
- fn sign(&self, message: &[u8]) -> Result<Box<[u8]>> {
- sign_with_attested_key(self.as_ref(), message)
- }
-}
-
-impl AsRef<AVmAttestationResult> for AttestationResult {
- fn as_ref(&self) -> &AVmAttestationResult {
- // SAFETY: This field is private, and only populated with a successful call to
- // `AVmPayload_requestAttestation`.
- unsafe { self.0.as_ref() }
- }
-}
-
-impl Drop for AttestationResult {
- fn drop(&mut self) {
- // SAFETY: This field is private, and only populated with a successful call to
- // `AVmPayload_requestAttestation`, and not freed elsewhere.
- unsafe { AVmAttestationResult_free(self.0.as_ptr()) };
- }
-}
-
-fn get_certificate_count(res: &AVmAttestationResult) -> usize {
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed.
- unsafe { AVmAttestationResult_getCertificateCount(res) }
-}
-
-fn get_certificate_at(res: &AVmAttestationResult, index: usize) -> Result<Box<[u8]>> {
- let size =
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed.
- unsafe { AVmAttestationResult_getCertificateAt(res, index, ptr::null_mut(), 0) };
- let mut cert = vec![0u8; size];
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed. This function only writes within the bounds of `cert`.
- // And `cert` cannot overlap `res` because we just allocated it.
- let size = unsafe {
- AVmAttestationResult_getCertificateAt(
- res,
- index,
- cert.as_mut_ptr() as *mut c_void,
- cert.len(),
- )
- };
- ensure!(size == cert.len());
- Ok(cert.into_boxed_slice())
-}
-
-fn get_private_key(res: &AVmAttestationResult) -> Result<Box<[u8]>> {
- let size =
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed.
- unsafe { AVmAttestationResult_getPrivateKey(res, ptr::null_mut(), 0) };
- let mut private_key = vec![0u8; size];
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed. This function only writes within the bounds of `private_key`.
- // And `private_key` cannot overlap `res` because we just allocated it.
- let size = unsafe {
- AVmAttestationResult_getPrivateKey(
- res,
- private_key.as_mut_ptr() as *mut c_void,
- private_key.len(),
- )
- };
- ensure!(size == private_key.len());
- Ok(private_key.into_boxed_slice())
-}
-
-fn sign_with_attested_key(res: &AVmAttestationResult, message: &[u8]) -> Result<Box<[u8]>> {
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed.
- let size = unsafe {
- AVmAttestationResult_sign(
- res,
- message.as_ptr() as *const c_void,
- message.len(),
- ptr::null_mut(),
- 0,
- )
- };
- let mut signature = vec![0u8; size];
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed. This function only writes within the bounds of `signature`.
- // And `signature` cannot overlap `res` because we just allocated it.
- let size = unsafe {
- AVmAttestationResult_sign(
- res,
- message.as_ptr() as *const c_void,
- message.len(),
- signature.as_mut_ptr() as *mut c_void,
- signature.len(),
- )
- };
- ensure!(size == signature.len());
- Ok(signature.into_boxed_slice())
-}
-
-fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
- // SAFETY: The function only reads the given enum status and returns a pointer to a
- // static string.
- let message = unsafe { AVmAttestationStatus_toString(status) };
- // SAFETY: The pointer returned by `AVmAttestationStatus_toString` is guaranteed to
- // point to a valid C String that lives forever.
- unsafe { CStr::from_ptr(message) }
-}
diff --git a/service_vm/test_apk/Android.bp b/service_vm/test_apk/Android.bp
index 1ba156f..58b394a 100644
--- a/service_vm/test_apk/Android.bp
+++ b/service_vm/test_apk/Android.bp
@@ -39,7 +39,7 @@
"libanyhow",
"libavflog",
"liblog_rust",
- "libvm_payload_bindgen",
+ "libvm_payload_rs",
],
}
diff --git a/service_vm/test_apk/src/native/main.rs b/service_vm/test_apk/src/native/main.rs
index 00ddff8..52635ad 100644
--- a/service_vm/test_apk/src/native/main.rs
+++ b/service_vm/test_apk/src/native/main.rs
@@ -14,35 +14,26 @@
//! Main executable of VM attestation for end-to-end testing.
-use anyhow::{anyhow, ensure, Result};
+use anyhow::Result;
use avflog::LogResult;
use com_android_virt_vm_attestation_testservice::{
aidl::com::android::virt::vm_attestation::testservice::IAttestationService::{
AttestationStatus::AttestationStatus, BnAttestationService, IAttestationService,
SigningResult::SigningResult, PORT,
},
- binder::{self, unstable_api::AsNative, BinderFeatures, Interface, IntoBinderResult, Strong},
+ binder::{self, BinderFeatures, Interface, IntoBinderResult, Strong},
};
use log::{error, info};
use std::{
- ffi::{c_void, CStr},
panic,
- ptr::{self, NonNull},
- result,
sync::{Arc, Mutex},
};
-use vm_payload_bindgen::{
- AIBinder, AVmAttestationResult, AVmAttestationResult_free,
- AVmAttestationResult_getCertificateAt, AVmAttestationResult_getCertificateCount,
- AVmAttestationResult_getPrivateKey, AVmAttestationResult_sign, AVmAttestationStatus,
- AVmAttestationStatus_toString, AVmPayload_notifyPayloadReady, AVmPayload_requestAttestation,
- AVmPayload_requestAttestationForTesting, AVmPayload_runVsockRpcServer,
-};
+use vm_payload::{AttestationError, AttestationResult};
-/// Entry point of the Service VM client.
-#[allow(non_snake_case)]
-#[no_mangle]
-pub extern "C" fn AVmPayload_main() {
+vm_payload::main!(main);
+
+// Entry point of the Service VM client.
+fn main() {
android_logger::init_once(
android_logger::Config::default()
.with_tag("service_vm_client")
@@ -61,18 +52,7 @@
fn try_main() -> Result<()> {
info!("Welcome to Service VM Client!");
- let mut service = AttestationService::new_binder().as_binder();
- let service = service.as_native_mut() as *mut AIBinder;
- let param = ptr::null_mut();
- // SAFETY: We hold a strong pointer, so the raw pointer remains valid. The bindgen AIBinder
- // is the same type as `sys::AIBinder`. It is safe for `on_ready` to be invoked at any time,
- // with any parameter.
- unsafe { AVmPayload_runVsockRpcServer(service, PORT.try_into()?, Some(on_ready), param) };
-}
-
-extern "C" fn on_ready(_param: *mut c_void) {
- // SAFETY: It is safe to call `AVmPayload_notifyPayloadReady` at any time.
- unsafe { AVmPayload_notifyPayloadReady() };
+ vm_payload::run_single_vsock_service(AttestationService::new_binder(), PORT.try_into()?)
}
struct AttestationService {
@@ -88,11 +68,11 @@
}
}
+#[allow(non_snake_case)]
impl IAttestationService for AttestationService {
fn requestAttestationForTesting(&self) -> binder::Result<()> {
const CHALLENGE: &[u8] = &[0xaa; 32];
- let res = AttestationResult::request_attestation_for_testing(CHALLENGE)
- .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))
+ let res = vm_payload::restricted::request_attestation_for_testing(CHALLENGE)
.with_log()
.or_service_specific_exception(-1)?;
*self.res.lock().unwrap() = Some(res);
@@ -104,218 +84,46 @@
challenge: &[u8],
message: &[u8],
) -> binder::Result<SigningResult> {
- let res = match AttestationResult::request_attestation(challenge) {
+ let res: AttestationResult = match vm_payload::request_attestation(challenge) {
Ok(res) => res,
- Err(status) => {
- let status = to_attestation_status(status);
+ Err(e) => {
+ let status = to_attestation_status(e);
return Ok(SigningResult { certificateChain: vec![], signature: vec![], status });
}
};
- let certificate_chain =
- res.certificate_chain().with_log().or_service_specific_exception(-1)?;
+
+ let certificate_chain: Vec<u8> = res.certificate_chain().flatten().collect();
let status = AttestationStatus::OK;
- let signature = res.sign(message).with_log().or_service_specific_exception(-1)?;
+ let signature = res.sign_message(message);
+
Ok(SigningResult { certificateChain: certificate_chain, signature, status })
}
fn validateAttestationResult(&self) -> binder::Result<()> {
// TODO(b/191073073): Returns the attestation result to the host for validation.
- self.res.lock().unwrap().as_ref().unwrap().log().or_service_specific_exception(-1)
- }
-}
-
-fn to_attestation_status(status: AVmAttestationStatus) -> AttestationStatus {
- match status {
- AVmAttestationStatus::ATTESTATION_OK => AttestationStatus::OK,
- AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE => {
- AttestationStatus::ERROR_INVALID_CHALLENGE
- }
- AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED => {
- AttestationStatus::ERROR_ATTESTATION_FAILED
- }
- AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => AttestationStatus::ERROR_UNSUPPORTED,
- }
-}
-
-#[derive(Debug)]
-struct AttestationResult(NonNull<AVmAttestationResult>);
-
-// Safety: `AttestationResult` is not `Send` because it contains a raw pointer to a C struct.
-unsafe impl Send for AttestationResult {}
-
-impl AttestationResult {
- fn request_attestation_for_testing(
- challenge: &[u8],
- ) -> result::Result<Self, AVmAttestationStatus> {
- let mut res: *mut AVmAttestationResult = ptr::null_mut();
- // SAFETY: It is safe as we only read the challenge within its bounds and the
- // function does not retain any reference to it.
- let status = unsafe {
- AVmPayload_requestAttestationForTesting(
- challenge.as_ptr() as *const c_void,
- challenge.len(),
- &mut res,
- )
- };
- if status == AVmAttestationStatus::ATTESTATION_OK {
- info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
- let res = NonNull::new(res).expect("The attestation result is null");
- Ok(Self(res))
- } else {
- Err(status)
- }
- }
-
- fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
- let mut res: *mut AVmAttestationResult = ptr::null_mut();
- // SAFETY: It is safe as we only read the challenge within its bounds and the
- // function does not retain any reference to it.
- let status = unsafe {
- AVmPayload_requestAttestation(
- challenge.as_ptr() as *const c_void,
- challenge.len(),
- &mut res,
- )
- };
- if status == AVmAttestationStatus::ATTESTATION_OK {
- info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
- let res = NonNull::new(res).expect("The attestation result is null");
- Ok(Self(res))
- } else {
- Err(status)
- }
- }
-
- fn certificate_chain(&self) -> Result<Vec<u8>> {
- let num_certs = get_certificate_count(self.as_ref());
- let mut certs = Vec::new();
- for i in 0..num_certs {
- certs.extend(get_certificate_at(self.as_ref(), i)?.iter());
- }
- Ok(certs)
- }
-
- fn private_key(&self) -> Result<Box<[u8]>> {
- get_private_key(self.as_ref())
- }
-
- fn sign(&self, message: &[u8]) -> Result<Vec<u8>> {
- sign_with_attested_key(self.as_ref(), message)
- }
-
- fn log(&self) -> Result<()> {
- let cert_chain = self.certificate_chain()?;
- info!("Attestation result certificateChain = {:?}", cert_chain);
-
- let private_key = self.private_key()?;
- info!("Attestation result privateKey = {:?}", private_key);
-
- let message = b"Hello from Service VM client";
- info!("Signing message: {:?}", message);
- let signature = self.sign(message)?;
- info!("Signature: {:?}", signature);
+ log(self.res.lock().unwrap().as_ref().unwrap());
Ok(())
}
}
-impl AsRef<AVmAttestationResult> for AttestationResult {
- fn as_ref(&self) -> &AVmAttestationResult {
- // SAFETY: This field is private, and only populated with a successful call to
- // `AVmPayload_requestAttestation`.
- unsafe { self.0.as_ref() }
+fn log(res: &AttestationResult) {
+ for (i, cert) in res.certificate_chain().enumerate() {
+ info!("Attestation result certificate {i} = {cert:?}");
}
+
+ let private_key = res.private_key();
+ info!("Attestation result privateKey = {private_key:?}");
+
+ let message = b"Hello from Service VM client";
+ info!("Signing message: {message:?}");
+ let signature = res.sign_message(message);
+ info!("Signature: {signature:?}");
}
-impl Drop for AttestationResult {
- fn drop(&mut self) {
- // SAFETY: This field is private, and only populated with a successful call to
- // `AVmPayload_requestAttestation`, and not freed elsewhere.
- unsafe { AVmAttestationResult_free(self.0.as_ptr()) };
+fn to_attestation_status(e: AttestationError) -> AttestationStatus {
+ match e {
+ AttestationError::InvalidChallenge => AttestationStatus::ERROR_INVALID_CHALLENGE,
+ AttestationError::AttestationFailed => AttestationStatus::ERROR_ATTESTATION_FAILED,
+ AttestationError::AttestationUnsupported => AttestationStatus::ERROR_UNSUPPORTED,
}
}
-
-fn get_certificate_count(res: &AVmAttestationResult) -> usize {
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed.
- unsafe { AVmAttestationResult_getCertificateCount(res) }
-}
-
-fn get_certificate_at(res: &AVmAttestationResult, index: usize) -> Result<Box<[u8]>> {
- let size =
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed.
- unsafe { AVmAttestationResult_getCertificateAt(res, index, ptr::null_mut(), 0) };
- let mut cert = vec![0u8; size];
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed. This function only writes within the bounds of `cert`.
- // And `cert` cannot overlap `res` because we just allocated it.
- let size = unsafe {
- AVmAttestationResult_getCertificateAt(
- res,
- index,
- cert.as_mut_ptr() as *mut c_void,
- cert.len(),
- )
- };
- ensure!(size == cert.len());
- Ok(cert.into_boxed_slice())
-}
-
-fn get_private_key(res: &AVmAttestationResult) -> Result<Box<[u8]>> {
- let size =
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed.
- unsafe { AVmAttestationResult_getPrivateKey(res, ptr::null_mut(), 0) };
- let mut private_key = vec![0u8; size];
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed. This function only writes within the bounds of `private_key`.
- // And `private_key` cannot overlap `res` because we just allocated it.
- let size = unsafe {
- AVmAttestationResult_getPrivateKey(
- res,
- private_key.as_mut_ptr() as *mut c_void,
- private_key.len(),
- )
- };
- ensure!(size == private_key.len());
- Ok(private_key.into_boxed_slice())
-}
-
-fn sign_with_attested_key(res: &AVmAttestationResult, message: &[u8]) -> Result<Vec<u8>> {
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed.
- let size = unsafe {
- AVmAttestationResult_sign(
- res,
- message.as_ptr() as *const c_void,
- message.len(),
- ptr::null_mut(),
- 0,
- )
- };
- let mut signature = vec![0u8; size];
- // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
- // before getting freed. This function only writes within the bounds of `signature`.
- // And `signature` cannot overlap `res` because we just allocated it.
- let size = unsafe {
- AVmAttestationResult_sign(
- res,
- message.as_ptr() as *const c_void,
- message.len(),
- signature.as_mut_ptr() as *mut c_void,
- signature.len(),
- )
- };
- ensure!(size <= signature.len());
- signature.truncate(size);
- Ok(signature)
-}
-
-fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
- // SAFETY: The function only reads the given enum status and returns a pointer to a
- // static string.
- let message = unsafe { AVmAttestationStatus_toString(status) };
- // SAFETY: The pointer returned by `AVmAttestationStatus_toString` is guaranteed to
- // point to a valid C String that lives forever.
- unsafe { CStr::from_ptr(message) }
-}
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index 413ffe4..5ede699 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -22,8 +22,10 @@
"MicrodroidTestNativeLib",
"libiovsock_host_jni",
],
- jni_uses_platform_apis: true,
- sdk_version: "test_current",
+ libs: [
+ "framework-virtualization.impl",
+ ],
+ platform_apis: true,
use_embedded_native_libs: true,
compile_multilib: "64",
required: ["perf-setup"],
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 9cc1b7b..b646ea1 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -16,8 +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.CPU_TOPOLOGY_ONE_CPU;
import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
@@ -34,10 +34,10 @@
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.os.Process;
import android.os.RemoteException;
+import android.system.Os;
import android.system.virtualmachine.VirtualMachine;
import android.system.virtualmachine.VirtualMachineConfig;
import android.system.virtualmachine.VirtualMachineException;
-import android.system.Os;
import android.system.virtualmachine.VirtualMachineManager;
import android.util.Log;
@@ -158,6 +158,7 @@
newVmConfigBuilderWithPayloadBinary("MicrodroidIdleNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_NONE)
.setMemoryBytes(mem * ONE_MEBI)
+ .setShouldUseHugepages(true)
.build();
// returns true if succeeded at least once.
@@ -233,6 +234,8 @@
for (int i = 0; i < trialCount; i++) {
VirtualMachineConfig.Builder builder =
newVmConfigBuilderWithPayloadBinary("MicrodroidIdleNativeLib.so")
+ .setShouldBoostUclamp(true)
+ .setShouldUseHugepages(true)
.setMemoryBytes(256 * ONE_MEBI)
.setDebugLevel(DEBUG_LEVEL_NONE);
if (fullDebug) {
@@ -345,6 +348,8 @@
VirtualMachineConfig config =
newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
.setDebugLevel(DEBUG_LEVEL_NONE)
+ .setShouldBoostUclamp(true)
+ .setShouldUseHugepages(true)
.build();
List<Double> transferRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
@@ -371,6 +376,7 @@
VirtualMachineConfig config =
newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
.setDebugLevel(DEBUG_LEVEL_NONE)
+ .setShouldUseHugepages(true)
.build();
List<Double> readRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
@@ -522,6 +528,7 @@
VirtualMachineConfig config =
newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
.setDebugLevel(DEBUG_LEVEL_NONE)
+ .setShouldUseHugepages(true)
.setMemoryBytes(256 * ONE_MEBI)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
@@ -608,6 +615,7 @@
VirtualMachineConfig config =
newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
.setDebugLevel(DEBUG_LEVEL_NONE)
+ .setShouldUseHugepages(true)
.setMemoryBytes(256 * ONE_MEBI)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
@@ -727,6 +735,8 @@
VirtualMachineConfig config =
newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_NONE)
+ .setShouldUseHugepages(true)
+ .setShouldBoostUclamp(true)
.build();
List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS);
@@ -775,6 +785,8 @@
VirtualMachineConfig config =
newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_NONE)
+ .setShouldBoostUclamp(true)
+ .setShouldUseHugepages(true)
.build();
List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS);
@@ -833,6 +845,7 @@
VirtualMachineConfig config =
newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
.setDebugLevel(DEBUG_LEVEL_NONE)
+ .setShouldUseHugepages(true)
.build();
List<Double> vmKillTime = new ArrayList<>(TEST_TRIAL_COUNT);
diff --git a/tests/ferrochrome/Android.bp b/tests/ferrochrome/Android.bp
new file mode 100644
index 0000000..f165b8f
--- /dev/null
+++ b/tests/ferrochrome/Android.bp
@@ -0,0 +1,29 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+sh_test_host {
+ name: "ferrochrome-tests",
+ src: ":ferrochrome-tests.sh",
+ test_suites: ["general-tests"],
+ test_options: {
+ unit_test: false,
+ },
+ per_testcase_directory: true,
+ data: ["assets/vm_config.json"],
+ data_bins: ["ferrochrome-precondition-checker.sh"],
+}
+
+// Workaround for enabling verbose logging only on CI
+genrule {
+ name: "ferrochrome-tests.sh",
+ srcs: ["ferrochrome.sh"],
+ out: ["ferrochrome-tests"],
+ // This breaks shebang, but test will execute the script with bash
+ cmd: "echo \"set -x\" > $(out); cat $(in) >> $(out)",
+}
+
+sh_binary_host {
+ name: "ferrochrome-precondition-checker.sh",
+ src: "ferrochrome-precondition-checker.sh",
+}
diff --git a/tests/ferrochrome/AndroidTest.xml b/tests/ferrochrome/AndroidTest.xml
new file mode 100644
index 0000000..79cbe72
--- /dev/null
+++ b/tests/ferrochrome/AndroidTest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2024 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.
+-->
+<configuration description="Host driven tests for ferrochrome">
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+ <!-- 'adb root' to enable vmlauncher -->
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
+ <option name="force-root" value="true"/>
+ </target_preparer>
+
+ <!-- Check assumptions here, because we can't skip tests in shell test -->
+ <target_preparer class="com.android.tradefed.targetprep.RunHostScriptTargetPreparer">
+ <option name="script-file" value="ferrochrome-precondition-checker.sh" />
+ </target_preparer>
+
+ <!-- Explicitly clean up ferrochrome image when done.
+ It's too large (6.5G+), so this may break further tests. -->
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="throw-if-cmd-fail" value="false" />
+ <option name="run-command" value="mkdir /data/local/tmp/ferrochrome" />
+ <option name="teardown-command" value="pkill vmlauncher" />
+ <option name="teardown-command" value="rm -rf /data/local/tmp/ferrochrome" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.binary.ExecutableHostTest" >
+ <option name="binary" value="ferrochrome-tests" />
+ <option name="relative-path-execution" value="true" />
+ <option name="runtime-hint" value="10m" />
+ <option name="per-binary-timeout" value="20m" />
+ </test>
+</configuration>
+
diff --git a/tests/ferrochrome/assets/vm_config.json b/tests/ferrochrome/assets/vm_config.json
new file mode 100644
index 0000000..1d32463
--- /dev/null
+++ b/tests/ferrochrome/assets/vm_config.json
@@ -0,0 +1,17 @@
+{
+ "name": "cros",
+ "disks": [
+ {
+ "image": "/data/local/tmp/ferrochrome/chromiumos_test_image.bin",
+ "partitions": [],
+ "writable": true
+ }
+ ],
+ "params": "root=/dev/vda3 rootwait noinitrd ro enforcing=0 cros_debug cros_secure",
+ "protected": false,
+ "cpu_topology": "match_host",
+ "platform_version": "~1.0",
+ "memory_mib" : 8096,
+ "console_input_device": "ttyS0"
+}
+
diff --git a/tests/ferrochrome/ferrochrome-precondition-checker.sh b/tests/ferrochrome/ferrochrome-precondition-checker.sh
new file mode 100644
index 0000000..d3f7f5a
--- /dev/null
+++ b/tests/ferrochrome/ferrochrome-precondition-checker.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+
+# Copyright 2024 Google Inc. All rights reserved.
+#
+# 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.
+
+
+## Precondition checks for running ferrochrome
+## Used by CI for skipping tests.
+
+REQUIRED_DISK_SPACE=7340032 # Requires 7G, while image is 6.5G
+
+# `adb root` always returns exit code 0
+if [[ "$(adb root)" == *"cannot"* ]]; then
+ >&2 echo "Failed to run adb root"
+ exit 1
+fi
+
+# `pm resolve-activity` always returns exit code 0
+resolved_activity=$(adb shell pm resolve-activity -a android.virtualization.VM_LAUNCHER)
+if [[ "${resolved_activity}" == "No activity found" ]]; then
+ >&2 echo "Failed to find vmlauncher"
+ exit 1
+fi
+
+free_space=$(adb shell df /data/local | tail -1 | awk '{print $4}')
+if [[ ${free_space} -lt ${REQUIRED_DISK_SPACE} ]]; then
+ >&2 echo "Insufficient space on DUT. Need ${REQUIRED_DISK_SPACE}, but was ${free_space}"
+ exit 1
+fi
+
+free_space=$(df /tmp | tail -1 | awk '{print $4}')
+if [[ ${free_space} -lt ${REQUIRED_DISK_SPACE} ]]; then
+ >&2 echo "Insufficient space on host. Need ${REQUIRED_DISK_SPACE}, but was ${free_space}"
+ exit 1
+fi
+
+cpu_abi=$(adb shell getprop ro.product.cpu.abi)
+if [[ "${cpu_abi}" != "arm64"* ]]; then
+ >&2 echo "Unsupported architecture. Requires arm64, but was ${cpu_abi}"
+ exit 1
+fi
+
+device=$(adb shell getprop ro.product.vendor.device)
+if [[ "${device}" == "vsock_"* ]]; then
+ >&2 echo "Unsupported device. Cuttlefish isn't supported"
+ exit 1
+fi
diff --git a/tests/ferrochrome/ferrochrome.sh b/tests/ferrochrome/ferrochrome.sh
new file mode 100755
index 0000000..d72e882
--- /dev/null
+++ b/tests/ferrochrome/ferrochrome.sh
@@ -0,0 +1,149 @@
+#!/bin/bash
+
+# Copyright 2024 Google Inc. All rights reserved.
+#
+# 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.
+
+## Booting tests for ferrochrome
+## Keep this file synced with docs/custom_vm.md
+
+set -e
+
+FECR_GS_URL="https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public"
+FECR_DEFAULT_VERSION="R127-15916.0.0"
+FECR_DEVICE_DIR="/data/local/tmp/ferrochrome"
+FECR_CONFIG_PATH="/data/local/tmp/vm_config.json" # hardcoded at VmLauncherApp
+FECR_CONSOLE_LOG_PATH="/data/data/\${pkg_name}/files/console.log"
+FECR_BOOT_COMPLETED_LOG="Have fun and send patches!"
+FECR_BOOT_TIMEOUT="300" # 5 minutes (300 seconds)
+ACTION_NAME="android.virtualization.VM_LAUNCHER"
+
+fecr_clean_up() {
+ trap - INT
+
+ if [[ -d ${fecr_dir} && -z ${fecr_keep} ]]; then
+ rm -rf ${fecr_dir}
+ fi
+}
+
+print_usage() {
+ echo "ferochrome: Launches ferrochrome image"
+ echo ""
+ echo "By default, this downloads ferrochrome image with version ${FECR_DEFAULT_VERSION},"
+ echo "launches, and waits for boot completed."
+ echo "When done, removes downloaded image on host while keeping pushed image on device."
+ echo ""
+ echo "Usage: ferrochrome [options]"
+ echo ""
+ echo "Options"
+ echo " --help or -h: This message"
+ echo " --dir DIR: Use ferrochrome images at the dir instead of downloading"
+ echo " --verbose: Verbose log message (set -x)"
+ echo " --skip: Skipping downloading and/or pushing images"
+ echo " --version \${version}: ferrochrome version to be downloaded"
+ echo " --keep: Keep downloaded ferrochrome image"
+}
+
+fecr_version=""
+fecr_dir=""
+fecr_keep=""
+fecr_skip=""
+fecr_script_path=$(dirname ${0})
+fecr_verbose=""
+
+# Parse parameters
+while (( "${#}" )); do
+ case "${1}" in
+ --verbose)
+ fecr_verbose="true"
+ ;;
+ --version)
+ shift
+ fecr_version="${1}"
+ ;;
+ --dir)
+ shift
+ fecr_dir="${1}"
+ fecr_keep="true"
+ ;;
+ --keep)
+ fecr_keep="true"
+ ;;
+ --skip)
+ fecr_skip="true"
+ ;;
+ -h|--help)
+ print_usage
+ exit 0
+ ;;
+ *)
+ print_usage
+ exit 1
+ ;;
+ esac
+ shift
+done
+
+trap fecr_clean_up INT
+trap fecr_clean_up EXIT
+
+if [[ -n "${fecr_verbose}" ]]; then
+ set -x
+fi
+
+. "${fecr_script_path}/ferrochrome-precondition-checker.sh"
+
+resolved_activities=$(adb shell pm query-activities --components -a ${ACTION_NAME})
+
+if [[ "$(echo ${resolved_activities} | wc -l)" != "1" ]]; then
+ >&2 echo "Multiple VM launchers exists"
+ exit 1
+fi
+
+pkg_name=$(dirname ${resolved_activities})
+
+adb shell pm grant ${pkg_name} android.permission.USE_CUSTOM_VIRTUAL_MACHINE > /dev/null
+adb shell pm clear ${pkg_name} > /dev/null
+
+if [[ -z "${fecr_skip}" ]]; then
+ if [[ -z "${fecr_dir}" ]]; then
+ # Download fecr image archive, and extract necessary files
+ # DISCLAIMER: Image is too large (1.5G+ for compressed, 6.5G+ for uncompressed), so can't submit.
+ fecr_dir=$(mktemp -d)
+
+ echo "Downloading & extracting ferrochrome image to ${fecr_dir}"
+ fecr_version=${fecr_version:-${FECR_DEFAULT_VERSION}}
+ curl ${FECR_GS_URL}/${fecr_version}/chromiumos_test_image.tar.xz | tar xfJ - -C ${fecr_dir}
+ fi
+
+ echo "Pushing ferrochrome image to ${FECR_DEVICE_DIR}"
+ adb shell mkdir -p ${FECR_DEVICE_DIR} > /dev/null || true
+ adb push ${fecr_dir}/chromiumos_test_image.bin ${FECR_DEVICE_DIR}
+ adb push ${fecr_script_path}/assets/vm_config.json ${FECR_CONFIG_PATH}
+fi
+
+echo "Starting ferrochrome"
+adb shell am start-activity -a ${ACTION_NAME} > /dev/null
+
+log_path="/data/data/${pkg_name}/files/console.log"
+fecr_start_time=${EPOCHSECONDS}
+
+while [[ $((EPOCHSECONDS - fecr_start_time)) -lt ${FECR_BOOT_TIMEOUT} ]]; do
+ adb shell grep -sF \""${FECR_BOOT_COMPLETED_LOG}"\" "${log_path}" && exit 0
+ sleep 10
+done
+
+>&2 echo "Ferrochrome failed to boot. Dumping console log"
+>&2 adb shell cat ${log_path}
+
+exit 1
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 e02db39..026cf3f 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
@@ -20,8 +20,8 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
-import static org.junit.Assume.assumeTrue;
import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import android.app.Instrumentation;
import android.app.UiAutomation;
@@ -191,6 +191,9 @@
assume().withMessage("Skip where protected VMs aren't supported")
.that(capabilities & VirtualMachineManager.CAPABILITY_PROTECTED_VM)
.isNotEqualTo(0);
+ assume().withMessage("Testing protected VMs on GSI isn't supported. b/272443823")
+ .that(isGsi())
+ .isFalse();
} else {
assume().withMessage("Skip where VMs aren't supported")
.that(capabilities & VirtualMachineManager.CAPABILITY_NON_PROTECTED_VM)
@@ -212,12 +215,17 @@
.that(mCtx.getPackageManager().hasSystemFeature(FEATURE_VIRTUALIZATION_FRAMEWORK))
.isTrue();
int vendorApiLevel = getVendorApiLevel();
- boolean isGsi = new File("/system/system_ext/etc/init/init.gsi.rc").exists();
+ boolean isGsi = isGsi();
+ Log.i(TAG, "isGsi = " + isGsi + ", vendor api level = " + vendorApiLevel);
assume().withMessage("GSI with vendor API level < 202404 may not support AVF")
.that(isGsi && vendorApiLevel < 202404)
.isFalse();
}
+ protected boolean isGsi() {
+ return new File("/system/system_ext/etc/init/init.gsi.rc").exists();
+ }
+
protected static int getVendorApiLevel() {
return SystemProperties.getInt("ro.board.api_level", 0);
}
@@ -548,6 +556,7 @@
public int mFileMode;
public int mMountFlags;
public String mConsoleInput;
+ public byte[] mInstanceSecret;
public void assertNoException() {
if (mException != null) {
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 471aea7..e32ff88 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -55,6 +55,7 @@
"MicrodroidExitNativeLib",
"MicrodroidPrivateLinkingNativeLib",
"MicrodroidCrashNativeLib",
+ "libmicrodroid_testlib_rust",
"libvm_attestation_test_payload",
],
min_sdk_version: "33",
@@ -166,3 +167,22 @@
header_libs: ["vm_payload_headers"],
stl: "libc++_static",
}
+
+// A payload written in Rust, using the Rust wrapper for the VM payload API.
+rust_ffi_shared {
+ name: "libmicrodroid_testlib_rust",
+ crate_name: "microdroid_testlib_rust",
+ defaults: ["avf_build_flags_rust"],
+ prefer_rlib: true,
+ srcs: ["src/native/testbinary.rs"],
+ compile_multilib: "both",
+ rustlibs: [
+ "com.android.microdroid.testservice-rust",
+ "libandroid_logger",
+ "libanyhow",
+ "libavflog",
+ "libcstr",
+ "liblog_rust",
+ "libvm_payload_rs",
+ ],
+}
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 12a46f7..c94f171 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -28,8 +28,6 @@
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 com.android.virt.vm_attestation.testservice.IAttestationService.AttestationStatus;
-import com.android.virt.vm_attestation.testservice.IAttestationService.SigningResult;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -39,6 +37,7 @@
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.util.stream.Collectors.toList;
+import android.app.ActivityManager;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.content.ComponentName;
@@ -70,8 +69,15 @@
import com.android.microdroid.testservice.IAppCallback;
import com.android.microdroid.testservice.ITestService;
import com.android.microdroid.testservice.IVmCallback;
+import com.android.virt.vm_attestation.testservice.IAttestationService.AttestationStatus;
+import com.android.virt.vm_attestation.testservice.IAttestationService.SigningResult;
import com.android.virt.vm_attestation.util.X509Utils;
+import co.nstant.in.cbor.CborDecoder;
+import co.nstant.in.cbor.model.Array;
+import co.nstant.in.cbor.model.DataItem;
+import co.nstant.in.cbor.model.MajorType;
+
import com.google.common.base.Strings;
import com.google.common.truth.BooleanSubject;
@@ -113,17 +119,13 @@
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
-import co.nstant.in.cbor.CborDecoder;
-import co.nstant.in.cbor.model.Array;
-import co.nstant.in.cbor.model.DataItem;
-import co.nstant.in.cbor.model.MajorType;
-
@RunWith(Parameterized.class)
public class MicrodroidTests extends MicrodroidDeviceTestBase {
private static final String TAG = "MicrodroidTests";
private static final String TEST_APP_PACKAGE_NAME = "com.android.microdroid.test";
private static final String VM_ATTESTATION_PAYLOAD_PATH = "libvm_attestation_test_payload.so";
private static final String VM_ATTESTATION_MESSAGE = "Hello RKP from AVF!";
+ private static final int ENCRYPTED_STORAGE_BYTES = 4_000_000;
@Rule public Timeout globalTimeout = Timeout.seconds(300);
@@ -196,6 +198,7 @@
tr.mSublibRunProp = ts.readProperty("debug.microdroid.app.sublib.run");
tr.mApkContentsPath = ts.getApkContentsPath();
tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
+ tr.mInstanceSecret = ts.insecurelyExposeVmInstanceSecret();
});
testResults.assertNoException();
assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
@@ -203,6 +206,7 @@
assertThat(testResults.mSublibRunProp).isEqualTo("true");
assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
+ assertThat(testResults.mInstanceSecret).hasLength(32);
}
@Test
@@ -571,7 +575,7 @@
assertThat(minimal.isProtectedVm()).isEqualTo(isProtectedVm());
assertThat(minimal.isEncryptedStorageEnabled()).isFalse();
assertThat(minimal.getEncryptedStorageBytes()).isEqualTo(0);
- assertThat(minimal.isVmOutputCaptured()).isEqualTo(false);
+ assertThat(minimal.isVmOutputCaptured()).isFalse();
assertThat(minimal.getOs()).isEqualTo("microdroid");
// Maximal has everything that can be set to some non-default value. (And has different
@@ -603,7 +607,7 @@
assertThat(maximal.isProtectedVm()).isEqualTo(isProtectedVm());
assertThat(maximal.isEncryptedStorageEnabled()).isTrue();
assertThat(maximal.getEncryptedStorageBytes()).isEqualTo(1_000_000);
- assertThat(maximal.isVmOutputCaptured()).isEqualTo(true);
+ assertThat(maximal.isVmOutputCaptured()).isTrue();
assertThat(maximal.getOs()).isEqualTo("microdroid_gki-android14-6.1");
assertThat(minimal.isCompatibleWith(maximal)).isFalse();
@@ -1583,7 +1587,7 @@
newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL);
if (encryptedStoreEnabled) {
- builder.setEncryptedStorageBytes(4_000_000);
+ builder.setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES);
}
VirtualMachineConfig config = builder.build();
String vmNameOrig = "test_vm_orig";
@@ -1637,7 +1641,7 @@
VirtualMachineConfig config =
newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
- .setEncryptedStorageBytes(4_000_000)
+ .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
@@ -1664,7 +1668,7 @@
VirtualMachineConfig config =
newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
- .setEncryptedStorageBytes(4_000_000)
+ .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
@@ -1771,7 +1775,7 @@
VirtualMachineConfig config =
newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
.setMemoryBytes(minMemoryRequired())
- .setEncryptedStorageBytes(4_000_000)
+ .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
.setDebugLevel(DEBUG_LEVEL_FULL)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_a", config);
@@ -2263,7 +2267,7 @@
VirtualMachineConfig vmConfig =
newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
.setDebugLevel(DEBUG_LEVEL_FULL)
- .setEncryptedStorageBytes(4_000_000)
+ .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
.build();
VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_encstore_no_exec", vmConfig);
@@ -2300,6 +2304,63 @@
}
}
+ @Test
+ public void createAndRunRustVm() throws Exception {
+ // This test is here mostly to exercise the Rust wrapper around the VM Payload API.
+ // We're testing the same functionality as in other tests, the only difference is
+ // that the payload is written in Rust.
+
+ assumeSupportedDevice();
+
+ VirtualMachineConfig config =
+ newVmConfigBuilderWithPayloadBinary("libmicrodroid_testlib_rust.so")
+ .setMemoryBytes(minMemoryRequired())
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .build();
+ VirtualMachine vm = forceCreateNewVirtualMachine("rust_vm", config);
+
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (ts, tr) -> {
+ tr.mAddInteger = ts.addInteger(37, 73);
+ tr.mApkContentsPath = ts.getApkContentsPath();
+ tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
+ tr.mInstanceSecret = ts.insecurelyExposeVmInstanceSecret();
+ });
+ testResults.assertNoException();
+ assertThat(testResults.mAddInteger).isEqualTo(37 + 73);
+ assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
+ assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
+ assertThat(testResults.mInstanceSecret).hasLength(32);
+ }
+
+ @Test
+ public void createAndRunRustVmWithEncryptedStorage() throws Exception {
+ // This test is here mostly to exercise the Rust wrapper around the VM Payload API.
+ // We're testing the same functionality as in other tests, the only difference is
+ // that the payload is written in Rust.
+
+ assumeSupportedDevice();
+
+ VirtualMachineConfig config =
+ newVmConfigBuilderWithPayloadBinary("libmicrodroid_testlib_rust.so")
+ .setMemoryBytes(minMemoryRequired())
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
+ .build();
+ VirtualMachine vm = forceCreateNewVirtualMachine("rust_vm", config);
+
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (ts, tr) -> tr.mEncryptedStoragePath = ts.getEncryptedStoragePath());
+ testResults.assertNoException();
+ assertThat(testResults.mEncryptedStoragePath).isEqualTo("/mnt/encryptedstore");
+ }
+
private VirtualMachineConfig buildVmConfigWithVendor(File vendorDiskImage) throws Exception {
return buildVmConfigWithVendor(vendorDiskImage, "MicrodroidTestNativeLib.so");
}
@@ -2452,6 +2513,52 @@
}
}
+ @Test
+ public void concurrentVms() throws Exception {
+ final long vmSize = minMemoryRequired();
+ final int numVMs = 8;
+ final long availableMem = getAvailableMemory();
+
+ // Let's not use more than half of the available memory
+ assume().withMessage("Available memory (" + availableMem + " bytes) too small")
+ .that((numVMs * vmSize) <= (availableMem / 2))
+ .isTrue();
+
+ VirtualMachine[] vms = new VirtualMachine[numVMs];
+ try {
+ for (int i = 0; i < numVMs; i++) {
+ VirtualMachineConfig config =
+ newVmConfigBuilderWithPayloadBinary("MicrodroidIdleNativeLib.so")
+ .setDebugLevel(DEBUG_LEVEL_NONE)
+ .setMemoryBytes(vmSize)
+ .build();
+
+ vms[i] = forceCreateNewVirtualMachine("test_concurrent_vms_" + i, config);
+ vms[i].run();
+ }
+
+ for (VirtualMachine vm : vms) {
+ assertThat(vm.getStatus()).isEqualTo(VirtualMachine.STATUS_RUNNING);
+ }
+
+ } finally {
+ // Ensure that VMs are all stopped. Otherwise we may try to reuse some of these for
+ // another run of this test with different parameters.
+ for (VirtualMachine vm : vms) {
+ if (vm != null) {
+ vm.close();
+ }
+ }
+ }
+ }
+
+ private long getAvailableMemory() {
+ ActivityManager am = getContext().getSystemService(ActivityManager.class);
+ ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
+ am.getMemoryInfo(memoryInfo);
+ return memoryInfo.availMem;
+ }
+
private VirtualMachineDescriptor toParcelFromParcel(VirtualMachineDescriptor descriptor) {
Parcel parcel = Parcel.obtain();
descriptor.writeToParcel(parcel, 0);
diff --git a/tests/testapk/src/native/testbinary.rs b/tests/testapk/src/native/testbinary.rs
new file mode 100644
index 0000000..85b411e
--- /dev/null
+++ b/tests/testapk/src/native/testbinary.rs
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2024 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.
+ */
+
+//! A VM payload that exists to allow testing of the Rust wrapper for the VM payload APIs.
+
+use anyhow::Result;
+use com_android_microdroid_testservice::{
+ aidl::com::android::microdroid::testservice::{
+ IAppCallback::IAppCallback,
+ ITestService::{BnTestService, ITestService, PORT},
+ },
+ binder::{BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, Strong},
+};
+use cstr::cstr;
+use log::{error, info};
+use std::panic;
+use std::process::exit;
+use std::string::String;
+use std::vec::Vec;
+
+vm_payload::main!(main);
+
+// Entry point of the Service VM client.
+fn main() {
+ android_logger::init_once(
+ android_logger::Config::default()
+ .with_tag("microdroid_testlib_rust")
+ .with_max_level(log::LevelFilter::Debug),
+ );
+ // Redirect panic messages to logcat.
+ panic::set_hook(Box::new(|panic_info| {
+ error!("{panic_info}");
+ }));
+ if let Err(e) = try_main() {
+ error!("failed with {:?}", e);
+ exit(1);
+ }
+}
+
+fn try_main() -> Result<()> {
+ info!("Welcome to the Rust test binary");
+
+ vm_payload::run_single_vsock_service(TestService::new_binder(), PORT.try_into()?)
+}
+
+struct TestService {}
+
+impl Interface for TestService {}
+
+impl TestService {
+ fn new_binder() -> Strong<dyn ITestService> {
+ BnTestService::new_binder(TestService {}, BinderFeatures::default())
+ }
+}
+
+impl ITestService for TestService {
+ fn quit(&self) -> BinderResult<()> {
+ exit(0)
+ }
+
+ fn addInteger(&self, a: i32, b: i32) -> BinderResult<i32> {
+ a.checked_add(b).ok_or_else(|| Status::new_exception(ExceptionCode::ILLEGAL_ARGUMENT, None))
+ }
+
+ fn getApkContentsPath(&self) -> BinderResult<String> {
+ Ok(vm_payload::apk_contents_path().to_string_lossy().to_string())
+ }
+
+ fn getEncryptedStoragePath(&self) -> BinderResult<String> {
+ Ok(vm_payload::encrypted_storage_path()
+ .map(|p| p.to_string_lossy().to_string())
+ .unwrap_or("".to_string()))
+ }
+
+ fn insecurelyExposeVmInstanceSecret(&self) -> BinderResult<Vec<u8>> {
+ let mut secret = vec![0u8; 32];
+ vm_payload::get_vm_instance_secret(b"identifier", secret.as_mut_slice());
+ Ok(secret)
+ }
+
+ // Everything below here is unimplemented. Implementations may be added as needed.
+
+ fn readProperty(&self, _: &str) -> BinderResult<String> {
+ unimplemented()
+ }
+ fn insecurelyExposeAttestationCdi(&self) -> BinderResult<Vec<u8>> {
+ unimplemented()
+ }
+ fn getBcc(&self) -> BinderResult<Vec<u8>> {
+ unimplemented()
+ }
+ fn runEchoReverseServer(&self) -> BinderResult<()> {
+ unimplemented()
+ }
+ fn getEffectiveCapabilities(&self) -> BinderResult<Vec<String>> {
+ unimplemented()
+ }
+ fn getUid(&self) -> BinderResult<i32> {
+ unimplemented()
+ }
+ fn writeToFile(&self, _: &str, _: &str) -> BinderResult<()> {
+ unimplemented()
+ }
+ fn readFromFile(&self, _: &str) -> BinderResult<String> {
+ unimplemented()
+ }
+ fn getFilePermissions(&self, _: &str) -> BinderResult<i32> {
+ unimplemented()
+ }
+ fn getMountFlags(&self, _: &str) -> BinderResult<i32> {
+ unimplemented()
+ }
+ fn requestCallback(&self, _: &Strong<dyn IAppCallback + 'static>) -> BinderResult<()> {
+ unimplemented()
+ }
+ fn readLineFromConsole(&self) -> BinderResult<String> {
+ unimplemented()
+ }
+}
+
+fn unimplemented<T>() -> BinderResult<T> {
+ let message = cstr!("Got a call to an unimplemented ITestService method in testbinary.rs");
+ error!("{message:?}");
+ Err(Status::new_exception(ExceptionCode::UNSUPPORTED_OPERATION, Some(message)))
+}
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 0055b3b..9df376a 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -17,7 +17,7 @@
use crate::{get_calling_pid, get_calling_uid, get_this_pid};
use crate::atom::{write_vm_booted_stats, write_vm_creation_stats};
use crate::composite::make_composite_image;
-use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, InputDeviceOption, PayloadState, VmContext, VmInstance, VmState};
+use crate::crosvm::{CrosvmConfig, DiskFile, DisplayConfig, GpuConfig, InputDeviceOption, PayloadState, VmContext, VmInstance, VmState};
use crate::debug_config::DebugConfig;
use crate::dt_overlay::{create_device_tree_overlay, VM_DT_OVERLAY_MAX_SIZE, VM_DT_OVERLAY_PATH};
use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images, add_microdroid_vendor_image};
@@ -401,68 +401,9 @@
check_gdb_allowed(config)?;
}
- // Currently, VirtMgr adds the host copy of reference DT & untrusted properties
- // (e.g. instance-id)
- let host_ref_dt = Path::new(VM_REFERENCE_DT_ON_HOST_PATH);
- let host_ref_dt = if host_ref_dt.exists()
- && read_dir(host_ref_dt).or_service_specific_exception(-1)?.next().is_some()
- {
- Some(host_ref_dt)
- } else {
- warn!("VM reference DT doesn't exist in host DT");
- None
- };
-
- let vendor_hashtree_digest = extract_vendor_hashtree_digest(config)
- .context("Failed to extract vendor hashtree digest")
- .or_service_specific_exception(-1)?;
-
- let trusted_props = if let Some(ref vendor_hashtree_digest) = vendor_hashtree_digest {
- info!(
- "Passing vendor hashtree digest to pvmfw. This will be rejected if it doesn't \
- match the trusted digest in the pvmfw config, causing the VM to fail to start."
- );
- vec![(
- cstr!("vendor_hashtree_descriptor_root_digest"),
- vendor_hashtree_digest.as_slice(),
- )]
- } else {
- vec![]
- };
-
- let instance_id;
- let mut untrusted_props = Vec::with_capacity(2);
- if cfg!(llpvm_changes) {
- instance_id = extract_instance_id(config);
- untrusted_props.push((cstr!("instance-id"), &instance_id[..]));
- let want_updatable = extract_want_updatable(config);
- if want_updatable && is_secretkeeper_supported() {
- // Let guest know that it can defer rollback protection to Secretkeeper by setting
- // an empty property in untrusted node in DT. This enables Updatable VMs.
- untrusted_props.push((cstr!("defer-rollback-protection"), &[]))
- }
- }
-
- let device_tree_overlay =
- if host_ref_dt.is_some() || !untrusted_props.is_empty() || !trusted_props.is_empty() {
- let dt_output = temporary_directory.join(VM_DT_OVERLAY_PATH);
- let mut data = [0_u8; VM_DT_OVERLAY_MAX_SIZE];
- let fdt = create_device_tree_overlay(
- &mut data,
- host_ref_dt,
- &untrusted_props,
- &trusted_props,
- )
- .map_err(|e| anyhow!("Failed to create DT overlay, {e:?}"))
- .or_service_specific_exception(-1)?;
- fs::write(&dt_output, fdt.as_slice()).or_service_specific_exception(-1)?;
- Some(File::open(dt_output).or_service_specific_exception(-1)?)
- } else {
- None
- };
+ let device_tree_overlay = maybe_create_device_tree_overlay(config, &temporary_directory)?;
let debug_config = DebugConfig::new(config);
-
let ramdump = if !uses_gki_kernel(config) && debug_config.is_ramdump_needed() {
Some(prepare_ramdump_file(&temporary_directory)?)
} else {
@@ -594,6 +535,16 @@
} else {
None
};
+ let gpu_config = if cfg!(paravirtualized_devices) {
+ config
+ .gpuConfig
+ .as_ref()
+ .map(GpuConfig::new)
+ .transpose()
+ .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?
+ } else {
+ None
+ };
let input_device_options = if cfg!(paravirtualized_devices) {
config
@@ -658,6 +609,8 @@
tap,
virtio_snd_backend,
console_input_device: config.consoleInputDevice.clone(),
+ boost_uclamp: config.boostUclamp,
+ gpu_config,
};
let instance = Arc::new(
VmInstance::new(
@@ -732,6 +685,67 @@
Err(anyhow!("No hashtree digest is extracted from microdroid vendor image"))
}
+fn maybe_create_device_tree_overlay(
+ config: &VirtualMachineConfig,
+ temporary_directory: &Path,
+) -> binder::Result<Option<File>> {
+ // Currently, VirtMgr adds the host copy of reference DT & untrusted properties
+ // (e.g. instance-id)
+ let host_ref_dt = Path::new(VM_REFERENCE_DT_ON_HOST_PATH);
+ let host_ref_dt = if host_ref_dt.exists()
+ && read_dir(host_ref_dt).or_service_specific_exception(-1)?.next().is_some()
+ {
+ Some(host_ref_dt)
+ } else {
+ warn!("VM reference DT doesn't exist in host DT");
+ None
+ };
+
+ let vendor_hashtree_digest = extract_vendor_hashtree_digest(config)
+ .context("Failed to extract vendor hashtree digest")
+ .or_service_specific_exception(-1)?;
+
+ let trusted_props = if let Some(ref vendor_hashtree_digest) = vendor_hashtree_digest {
+ info!(
+ "Passing vendor hashtree digest to pvmfw. This will be rejected if it doesn't \
+ match the trusted digest in the pvmfw config, causing the VM to fail to start."
+ );
+ vec![(cstr!("vendor_hashtree_descriptor_root_digest"), vendor_hashtree_digest.as_slice())]
+ } else {
+ vec![]
+ };
+
+ let instance_id;
+ let mut untrusted_props = Vec::with_capacity(2);
+ if cfg!(llpvm_changes) {
+ instance_id = extract_instance_id(config);
+ untrusted_props.push((cstr!("instance-id"), &instance_id[..]));
+ let want_updatable = extract_want_updatable(config);
+ if want_updatable && is_secretkeeper_supported() {
+ // Let guest know that it can defer rollback protection to Secretkeeper by setting
+ // an empty property in untrusted node in DT. This enables Updatable VMs.
+ untrusted_props.push((cstr!("defer-rollback-protection"), &[]))
+ }
+ }
+
+ let device_tree_overlay = if host_ref_dt.is_some()
+ || !untrusted_props.is_empty()
+ || !trusted_props.is_empty()
+ {
+ let dt_output = temporary_directory.join(VM_DT_OVERLAY_PATH);
+ let mut data = [0_u8; VM_DT_OVERLAY_MAX_SIZE];
+ let fdt =
+ create_device_tree_overlay(&mut data, host_ref_dt, &untrusted_props, &trusted_props)
+ .map_err(|e| anyhow!("Failed to create DT overlay, {e:?}"))
+ .or_service_specific_exception(-1)?;
+ fs::write(&dt_output, fdt.as_slice()).or_service_specific_exception(-1)?;
+ Some(File::open(dt_output).or_service_specific_exception(-1)?)
+ } else {
+ None
+ };
+ Ok(device_tree_overlay)
+}
+
fn write_zero_filler(zero_filler_path: &Path) -> Result<()> {
let file = OpenOptions::new()
.create_new(true)
@@ -958,6 +972,7 @@
vm_config.protectedVm = config.protectedVm;
vm_config.cpuTopology = config.cpuTopology;
vm_config.hugePages = config.hugePages || vm_payload_config.hugepages;
+ vm_config.boostUclamp = config.boostUclamp;
// Microdroid takes additional init ramdisk & (optionally) storage image
add_microdroid_system_images(config, instance_file, storage_image, os_name, &mut vm_config)?;
@@ -1225,6 +1240,10 @@
.or_service_specific_exception(-1)?;
Ok(vsock_stream_to_pfd(stream))
}
+
+ fn setHostConsoleName(&self, ptsname: &str) -> binder::Result<()> {
+ self.instance.vm_context.global_context.setHostConsoleName(ptsname)
+ }
}
impl Drop for VirtualMachine {
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 6408b84..47ef91a 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -14,10 +14,11 @@
//! Functions for running instances of `crosvm`.
-use crate::aidl::{remove_temporary_files, Cid, VirtualMachineCallbacks};
+use crate::aidl::{remove_temporary_files, Cid, GLOBAL_SERVICE, VirtualMachineCallbacks};
use crate::atom::{get_num_cpus, write_vm_exited_stats_sync};
use crate::debug_config::DebugConfig;
use anyhow::{anyhow, bail, Context, Error, Result};
+use binder::ParcelFileDescriptor;
use command_fds::CommandFdExt;
use lazy_static::lazy_static;
use libc::{sysconf, _SC_CLK_TCK};
@@ -34,7 +35,7 @@
use std::io::{self, Read};
use std::mem;
use std::num::{NonZeroU16, NonZeroU32};
-use std::os::unix::io::{AsRawFd, RawFd};
+use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
use std::os::unix::process::ExitStatusExt;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
@@ -46,6 +47,7 @@
MemoryTrimLevel::MemoryTrimLevel,
VirtualMachineAppConfig::DebugLevel::DebugLevel,
DisplayConfig::DisplayConfig as DisplayConfigParcelable,
+ GpuConfig::GpuConfig as GpuConfigParcelable,
};
use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IBoundDevice::IBoundDevice;
@@ -131,6 +133,8 @@
pub tap: Option<File>,
pub virtio_snd_backend: Option<String>,
pub console_input_device: Option<String>,
+ pub boost_uclamp: bool,
+ pub gpu_config: Option<GpuConfig>,
}
#[derive(Debug)]
@@ -153,6 +157,37 @@
}
}
+#[derive(Debug)]
+pub struct GpuConfig {
+ pub backend: Option<String>,
+ pub context_types: Option<Vec<String>>,
+ pub pci_address: Option<String>,
+ pub renderer_features: Option<String>,
+ pub renderer_use_egl: Option<bool>,
+ pub renderer_use_gles: Option<bool>,
+ pub renderer_use_glx: Option<bool>,
+ pub renderer_use_surfaceless: Option<bool>,
+ pub renderer_use_vulkan: Option<bool>,
+}
+
+impl GpuConfig {
+ pub fn new(raw_config: &GpuConfigParcelable) -> Result<GpuConfig> {
+ Ok(GpuConfig {
+ backend: raw_config.backend.clone(),
+ context_types: raw_config.contextTypes.clone().map(|context_types| {
+ context_types.iter().filter_map(|context_type| context_type.clone()).collect()
+ }),
+ pci_address: raw_config.pciAddress.clone(),
+ renderer_features: raw_config.rendererFeatures.clone(),
+ renderer_use_egl: Some(raw_config.rendererUseEgl),
+ renderer_use_gles: Some(raw_config.rendererUseGles),
+ renderer_use_glx: Some(raw_config.rendererUseGlx),
+ renderer_use_surfaceless: Some(raw_config.rendererUseSurfaceless),
+ renderer_use_vulkan: Some(raw_config.rendererUseVulkan),
+ })
+ }
+}
+
fn try_into_non_zero_u32(value: i32) -> Result<NonZeroU32> {
let u32_value = value.try_into()?;
NonZeroU32::new(u32_value).ok_or(anyhow!("value should be greater than 0"))
@@ -241,6 +276,8 @@
let detect_hangup = config.detect_hangup;
let (failure_pipe_read, failure_pipe_write) = create_pipe()?;
let vfio_devices = config.vfio_devices.clone();
+ let tap =
+ if let Some(tap_file) = &config.tap { Some(tap_file.try_clone()?) } else { None };
// If this fails and returns an error, `self` will be left in the `Failed` state.
let child =
@@ -255,7 +292,7 @@
let child_clone = child.clone();
let instance_clone = instance.clone();
let monitor_vm_exit_thread = Some(thread::spawn(move || {
- instance_clone.monitor_vm_exit(child_clone, failure_pipe_read, vfio_devices);
+ instance_clone.monitor_vm_exit(child_clone, failure_pipe_read, vfio_devices, tap);
}));
if detect_hangup {
@@ -279,7 +316,7 @@
#[derive(Debug)]
pub struct VmContext {
#[allow(dead_code)] // Keeps the global context alive
- global_context: Strong<dyn IGlobalVmContext>,
+ pub(crate) global_context: Strong<dyn IGlobalVmContext>,
#[allow(dead_code)] // Keeps the server alive
vm_server: RpcServer,
}
@@ -298,7 +335,7 @@
pub vm_state: Mutex<VmState>,
/// Global resources allocated for this VM.
#[allow(dead_code)] // Keeps the context alive
- vm_context: VmContext,
+ pub(crate) vm_context: VmContext,
/// The CID assigned to the VM for vsock communication.
pub cid: Cid,
/// Path to crosvm control socket
@@ -397,6 +434,7 @@
child: Arc<SharedChild>,
mut failure_pipe_read: File,
vfio_devices: Vec<VfioDevice>,
+ tap: Option<File>,
) {
let result = child.wait();
match &result {
@@ -456,6 +494,14 @@
error!("Error removing temporary files from {:?}: {}", self.temporary_directory, e);
});
+ if let Some(tap_file) = tap {
+ GLOBAL_SERVICE
+ .deleteTapInterface(&ParcelFileDescriptor::new(OwnedFd::from(tap_file)))
+ .unwrap_or_else(|e| {
+ error!("Error deleting TAP interface: {e:?}");
+ });
+ }
+
drop(vfio_devices); // Cleanup devices.
}
@@ -842,6 +888,8 @@
command.arg("--no-balloon");
}
+ let mut memory_mib = config.memory_mib;
+
if config.protected {
match system_properties::read(SYSPROP_CUSTOM_PVMFW_PATH)? {
Some(pvmfw_path) if !pvmfw_path.is_empty() => {
@@ -857,6 +905,12 @@
let swiotlb_size_mib = 2 * virtio_pci_device_count as u32;
command.arg("--swiotlb").arg(swiotlb_size_mib.to_string());
+ // b/346770542 for consistent "usable" memory across protected and non-protected VMs under
+ // pKVM.
+ if hypervisor_props::is_pkvm()? {
+ memory_mib = memory_mib.map(|m| m.saturating_add(swiotlb_size_mib));
+ }
+
// Workaround to keep crash_dump from trying to read protected guest memory.
// Context in b/238324526.
command.arg("--unmap-guest-memory-on-fork");
@@ -878,7 +932,7 @@
command.arg("--params").arg("console=hvc0");
}
- if let Some(memory_mib) = config.memory_mib {
+ if let Some(memory_mib) = memory_mib {
command.arg("--mem").arg(memory_mib.to_string());
}
@@ -988,21 +1042,48 @@
}
if cfg!(paravirtualized_devices) {
- if let Some(display_config) = &config.display_config {
- command.arg("--gpu")
- // TODO(b/331708504): support backend config as well
- .arg("backend=virglrenderer,context-types=virgl2,egl=true,surfaceless=true,glx=false,gles=true")
- .arg(format!("--gpu-display=mode=windowed[{},{}],dpi=[{},{}],refresh-rate={}", display_config.width, display_config.height, display_config.horizontal_dpi, display_config.vertical_dpi, display_config.refresh_rate))
- .arg(format!("--android-display-service={}", config.name));
+ if let Some(gpu_config) = &config.gpu_config {
+ let mut gpu_args = Vec::new();
+ if let Some(backend) = &gpu_config.backend {
+ gpu_args.push(format!("backend={}", backend));
+ }
+ if let Some(context_types) = &gpu_config.context_types {
+ gpu_args.push(format!("context-types={}", context_types.join(":")));
+ }
+ if let Some(pci_address) = &gpu_config.pci_address {
+ gpu_args.push(format!("pci-address={}", pci_address));
+ }
+ if let Some(renderer_features) = &gpu_config.renderer_features {
+ gpu_args.push(format!("renderer-features={}", renderer_features));
+ }
+ if gpu_config.renderer_use_egl.unwrap_or(false) {
+ gpu_args.push("egl=true".to_string());
+ }
+ if gpu_config.renderer_use_gles.unwrap_or(false) {
+ gpu_args.push("gles=true".to_string());
+ }
+ if gpu_config.renderer_use_glx.unwrap_or(false) {
+ gpu_args.push("glx=true".to_string());
+ }
+ if gpu_config.renderer_use_surfaceless.unwrap_or(false) {
+ gpu_args.push("surfaceless=true".to_string());
+ }
+ if gpu_config.renderer_use_vulkan.unwrap_or(false) {
+ gpu_args.push("vulkan=true".to_string());
+ }
+ command.arg(format!("--gpu={}", gpu_args.join(",")));
}
- }
-
- if cfg!(paravirtualized_devices) {
- // TODO(b/340376951): Remove this after tap in CrosvmConfig is connected to tethering.
- if rustutils::system_properties::read_bool("ro.crosvm.network.setup.done", false)
- .unwrap_or(false)
- {
- command.arg("--net").arg("tap-name=crosvm_tap");
+ if let Some(display_config) = &config.display_config {
+ command
+ .arg(format!(
+ "--gpu-display=mode=windowed[{},{}],dpi=[{},{}],refresh-rate={}",
+ display_config.width,
+ display_config.height,
+ display_config.horizontal_dpi,
+ display_config.vertical_dpi,
+ display_config.refresh_rate
+ ))
+ .arg(format!("--android-display-service={}", config.name));
}
}
@@ -1042,6 +1123,10 @@
command.arg("--hugepages");
}
+ if config.boost_uclamp {
+ command.arg("--boost-uclamp");
+ }
+
append_platform_devices(&mut command, &mut preserved_fds, &config)?;
debug!("Preserving FDs {:?}", preserved_fds);
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 0c39501..f9034af 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -30,6 +30,7 @@
"android.system.virtualizationservice-rust",
"android.system.virtualizationservice_internal-rust",
"android.system.virtualmachineservice-rust",
+ "android.system.vmtethering-rust",
"android.os.permissions_aidl-rust",
"libandroid_logger",
"libanyhow",
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index fb89772..bca4512 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -86,6 +86,26 @@
}
aidl_interface {
+ name: "android.system.vmtethering",
+ srcs: ["android/system/vmtethering/**/*.aidl"],
+ unstable: true,
+ backend: {
+ java: {
+ sdk_version: "module_current",
+ apex_available: [
+ "com.android.virt",
+ ],
+ },
+ rust: {
+ enabled: true,
+ apex_available: [
+ "com.android.virt",
+ ],
+ },
+ },
+}
+
+aidl_interface {
name: "android.system.virtualmachineservice",
srcs: ["android/system/virtualmachineservice/**/*.aidl"],
imports: [
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl
new file mode 100644
index 0000000..1cd4dc6
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.system.virtualizationservice;
+
+parcelable GpuConfig {
+ @nullable String backend;
+ @nullable String[] contextTypes;
+ @nullable String pciAddress;
+ @nullable String rendererFeatures;
+ boolean rendererUseEgl = false;
+ boolean rendererUseGles = false;
+ boolean rendererUseGlx = false;
+ boolean rendererUseSurfaceless = false;
+ boolean rendererUseVulkan = false;
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
index d76b586..d4001c8 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
@@ -47,4 +47,7 @@
/** Open a vsock connection to the CID of the VM on the given port. */
ParcelFileDescriptor connectVsock(int port);
+
+ /** Set the name of the peer end (ptsname) of the host console. */
+ void setHostConsoleName(in @utf8InCpp String pathname);
}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index f8b5087..234d8d0 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -25,6 +25,7 @@
const String FEATURE_DICE_CHANGES = "com.android.kvm.DICE_CHANGES";
const String FEATURE_LLPVM_CHANGES = "com.android.kvm.LLPVM_CHANGES";
const String FEATURE_MULTI_TENANT = "com.android.kvm.MULTI_TENANT";
+ const String FEATURE_NETWORK = "com.android.kvm.NETWORK";
const String FEATURE_REMOTE_ATTESTATION = "com.android.kvm.REMOTE_ATTESTATION";
const String FEATURE_VENDOR_MODULES = "com.android.kvm.VENDOR_MODULES";
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index a3f4b0f..ee39d75 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -139,4 +139,7 @@
* https://docs.kernel.org/admin-guide/mm/transhuge.html
*/
boolean hugePages;
+
+ /** Enable boost UClamp for less variance during testing/benchmarking */
+ boolean boostUclamp;
}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
index 870a342..9f033b1 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
@@ -33,4 +33,7 @@
* the PID may have been reused for a different process, so this should not be trusted.
*/
int requesterPid;
+
+ /** The peer end (ptsname) of the host console. */
+ @nullable @utf8InCpp String hostConsoleName;
}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index c927c9b..69664b4 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -18,6 +18,7 @@
import android.system.virtualizationservice.CpuTopology;
import android.system.virtualizationservice.DiskImage;
import android.system.virtualizationservice.DisplayConfig;
+import android.system.virtualizationservice.GpuConfig;
import android.system.virtualizationservice.InputDevice;
/** Raw configuration for running a VM. */
@@ -91,4 +92,9 @@
/** The serial device for VM console input. */
@nullable @utf8InCpp String consoleInputDevice;
+
+ /** Enable boost UClamp for less variance during testing/benchmarking */
+ boolean boostUclamp;
+
+ @nullable GpuConfig gpuConfig;
}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
index a4d5d19..ea52591 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
@@ -21,4 +21,7 @@
/** Get the path to the temporary folder of the VM. */
String getTemporaryDirectory();
+
+ /** Set the name of the peer end (ptsname) of the host console. */
+ void setHostConsoleName(@utf8InCpp String pathname);
}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index 4e6879d..0da7755 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -128,4 +128,10 @@
* @return file descriptor of the TAP network interface.
*/
ParcelFileDescriptor createTapInterface(String ifaceNameSuffix);
+
+ /**
+ * Delete TAP network interface created for a VM.
+ * @param file descriptor of the TAP network interface.
+ */
+ void deleteTapInterface(in ParcelFileDescriptor tapFd);
}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl
index 66739da..e3cc73a 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl
@@ -22,4 +22,10 @@
* @return file descriptor of the TAP network interface.
*/
ParcelFileDescriptor createTapInterface(String ifaceNameSuffix);
+
+ /**
+ * Delete TAP network interface created for a VM.
+ * @param file descriptor of the TAP network interface.
+ */
+ void deleteTapInterface(in ParcelFileDescriptor tapFd);
}
diff --git a/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl b/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl
new file mode 100644
index 0000000..0743ffa
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.vmtethering;
+
+interface IVmTethering {
+ /**
+ * Start VM tethering to provide external network to VM.
+ */
+ void enableVmTethering();
+
+ /**
+ * Terminate VM tethering that providing external network to VM.
+ */
+ void disableVmTethering();
+}
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 5592f14..acdb53a 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -25,6 +25,7 @@
use android_system_virtualizationservice::aidl::android::system::virtualizationservice;
use android_system_virtualizationservice_internal as android_vs_internal;
use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice;
+use android_system_vmtethering::aidl::android::system::vmtethering;
use android_vs_internal::aidl::android::system::virtualizationservice_internal;
use anyhow::{anyhow, ensure, Context, Result};
use avflog::LogResult;
@@ -33,7 +34,7 @@
LazyServiceGuard, ParcelFileDescriptor, Status, Strong,
};
use lazy_static::lazy_static;
-use libc::VMADDR_CID_HOST;
+use libc::{VMADDR_CID_HOST, VMADDR_CID_HYPERVISOR, VMADDR_CID_LOCAL};
use log::{error, info, warn};
use nix::unistd::{chown, Uid};
use openssl::x509::X509;
@@ -73,6 +74,7 @@
IVmnic::{BpVmnic, IVmnic},
};
use virtualmachineservice::IVirtualMachineService::VM_TOMBSTONES_SERVICE_PORT;
+use vmtethering::IVmTethering::{BpVmTethering, IVmTethering};
use vsock::{VsockListener, VsockStream};
/// The unique ID of a VM used (together with a port number) for vsock communication.
@@ -163,6 +165,9 @@
static ref NETWORK_SERVICE: Strong<dyn IVmnic> =
wait_for_interface(<BpVmnic as IVmnic>::get_descriptor())
.expect("Could not connect to Vmnic");
+ static ref TETHERING_SERVICE: Strong<dyn IVmTethering> =
+ wait_for_interface(<BpVmTethering as IVmTethering>::get_descriptor())
+ .expect("Could not connect to VmTethering");
}
fn is_valid_guest_cid(cid: Cid) -> bool {
@@ -282,11 +287,15 @@
.held_contexts
.iter()
.filter_map(|(_, inst)| Weak::upgrade(inst))
- .map(|vm| VirtualMachineDebugInfo {
- cid: vm.cid as i32,
- temporaryDirectory: vm.get_temp_dir().to_string_lossy().to_string(),
- requesterUid: vm.requester_uid as i32,
- requesterPid: vm.requester_debug_pid,
+ .map(|vm| {
+ let vm = vm.lock().unwrap();
+ VirtualMachineDebugInfo {
+ cid: vm.cid as i32,
+ temporaryDirectory: vm.get_temp_dir().to_string_lossy().to_string(),
+ requesterUid: vm.requester_uid as i32,
+ requesterPid: vm.requester_debug_pid,
+ hostConsoleName: vm.host_console_name.clone(),
+ }
})
.collect();
Ok(cids)
@@ -454,7 +463,7 @@
.context("Failed to allocate instance_id")
.or_service_specific_exception(-1)?;
let uid = get_calling_uid();
- info!("Allocated a VM's instance_id: {:?}, for uid: {:?}", hex::encode(id), uid);
+ info!("Allocated a VM's instance_id: {:?}..., for uid: {:?}", &hex::encode(id)[..8], uid);
let state = &mut *self.state.lock().unwrap();
if let Some(sk_state) = &mut state.sk_state {
let user_id = multiuser_get_user_id(uid);
@@ -509,7 +518,8 @@
Ok(())
}
- fn createTapInterface(&self, iface_name_suffix: &str) -> binder::Result<ParcelFileDescriptor> {
+ fn createTapInterface(&self, _iface_name_suffix: &str) -> binder::Result<ParcelFileDescriptor> {
+ check_internet_permission()?;
check_use_custom_virtual_machine()?;
if !cfg!(network) {
return Err(Status::new_exception_str(
@@ -518,7 +528,33 @@
))
.with_log();
}
- NETWORK_SERVICE.createTapInterface(iface_name_suffix)
+ // TODO(340377643): Use iface_name_suffix after introducing bridge interface, not fixed
+ // value.
+ let tap_fd = NETWORK_SERVICE.createTapInterface("fixed")?;
+
+ // TODO(340377643): Due to lack of implementation of creating bridge interface, tethering is
+ // enabled for TAP interface instead of bridge interface. After introducing creation of
+ // bridge interface in AVF, we should modify it.
+ TETHERING_SERVICE.enableVmTethering()?;
+
+ Ok(tap_fd)
+ }
+
+ fn deleteTapInterface(&self, tap_fd: &ParcelFileDescriptor) -> binder::Result<()> {
+ check_internet_permission()?;
+ check_use_custom_virtual_machine()?;
+ if !cfg!(network) {
+ return Err(Status::new_exception_str(
+ ExceptionCode::UNSUPPORTED_OPERATION,
+ Some("deleteTapInterface is not supported with the network feature disabled"),
+ ))
+ .with_log();
+ }
+
+ // TODO(340377643): Disabling tethering should be for bridge interface, not TAP interface.
+ TETHERING_SERVICE.disableVmTethering()?;
+
+ NETWORK_SERVICE.deleteTapInterface(tap_fd)
}
}
@@ -629,6 +665,8 @@
requester_uid: uid_t,
/// PID of the client who requested this VM instance.
requester_debug_pid: pid_t,
+ /// Name of the host console.
+ host_console_name: Option<String>,
}
impl GlobalVmInstance {
@@ -643,7 +681,7 @@
struct GlobalState {
/// VM contexts currently allocated to running VMs. A CID is never recycled as long
/// as there is a strong reference held by a GlobalVmContext.
- held_contexts: HashMap<Cid, Weak<GlobalVmInstance>>,
+ held_contexts: HashMap<Cid, Weak<Mutex<GlobalVmInstance>>>,
/// Cached read-only FD of VM DTBO file. Also serves as a lock for creating the file.
dtbo_file: Mutex<Option<File>>,
@@ -723,8 +761,13 @@
self.held_contexts.retain(|_, instance| instance.strong_count() > 0);
let cid = self.get_next_available_cid()?;
- let instance = Arc::new(GlobalVmInstance { cid, requester_uid, requester_debug_pid });
- create_temporary_directory(&instance.get_temp_dir(), Some(requester_uid))?;
+ let instance = Arc::new(Mutex::new(GlobalVmInstance {
+ cid,
+ requester_uid,
+ requester_debug_pid,
+ ..Default::default()
+ }));
+ create_temporary_directory(&instance.lock().unwrap().get_temp_dir(), Some(requester_uid))?;
self.held_contexts.insert(cid, Arc::downgrade(&instance));
let binder = GlobalVmContext { instance, ..Default::default() };
@@ -804,7 +847,7 @@
#[derive(Debug, Default)]
struct GlobalVmContext {
/// Strong reference to the context's instance data structure.
- instance: Arc<GlobalVmInstance>,
+ instance: Arc<Mutex<GlobalVmInstance>>,
/// Keeps our service process running as long as this VM context exists.
#[allow(dead_code)]
lazy_service_guard: LazyServiceGuard,
@@ -814,11 +857,16 @@
impl IGlobalVmContext for GlobalVmContext {
fn getCid(&self) -> binder::Result<i32> {
- Ok(self.instance.cid as i32)
+ Ok(self.instance.lock().unwrap().cid as i32)
}
fn getTemporaryDirectory(&self) -> binder::Result<String> {
- Ok(self.instance.get_temp_dir().to_string_lossy().to_string())
+ Ok(self.instance.lock().unwrap().get_temp_dir().to_string_lossy().to_string())
+ }
+
+ fn setHostConsoleName(&self, pathname: &str) -> binder::Result<()> {
+ self.instance.lock().unwrap().host_console_name = Some(pathname.to_string());
+ Ok(())
}
}
@@ -830,11 +878,21 @@
for incoming_stream in listener.incoming() {
let mut incoming_stream = match incoming_stream {
Err(e) => {
- warn!("invalid incoming connection: {:?}", e);
+ warn!("invalid incoming connection: {e:?}");
continue;
}
Ok(s) => s,
};
+ if let Ok(addr) = incoming_stream.peer_addr() {
+ let cid = addr.cid();
+ match cid {
+ VMADDR_CID_LOCAL | VMADDR_CID_HOST | VMADDR_CID_HYPERVISOR => {
+ warn!("Rejecting non-guest tombstone vsock connection from cid={cid}");
+ continue;
+ }
+ _ => info!("Vsock Stream connected to cid={cid} for tombstones"),
+ }
+ }
std::thread::spawn(move || {
if let Err(e) = handle_tombstone(&mut incoming_stream) {
error!("Failed to write tombstone- {:?}", e);
@@ -845,9 +903,6 @@
}
fn handle_tombstone(stream: &mut VsockStream) -> Result<()> {
- if let Ok(addr) = stream.peer_addr() {
- info!("Vsock Stream connected to cid={} for tombstones", addr.cid());
- }
let tb_connection =
TombstonedConnection::connect(std::process::id() as i32, DebuggerdDumpType::Tombstone)
.context("Failed to connect to tombstoned")?;
@@ -911,6 +966,12 @@
check_permission("android.permission.USE_CUSTOM_VIRTUAL_MACHINE")
}
+/// Check whether the caller of the current Binder method is allowed to create socket and
+/// establish connection between the VM and the Internet.
+fn check_internet_permission() -> binder::Result<()> {
+ check_permission("android.permission.INTERNET")
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/virtualizationservice/vmnic/src/aidl.rs b/virtualizationservice/vmnic/src/aidl.rs
index a206c25..03819b8 100644
--- a/virtualizationservice/vmnic/src/aidl.rs
+++ b/virtualizationservice/vmnic/src/aidl.rs
@@ -19,22 +19,20 @@
use binder::{self, Interface, IntoBinderResult, ParcelFileDescriptor};
use libc::{c_char, c_int, c_short, ifreq, IFF_NO_PI, IFF_TAP, IFF_UP, IFF_VNET_HDR, IFNAMSIZ};
use log::info;
-use nix::{ioctl_write_int_bad, ioctl_write_ptr_bad};
+use nix::ioctl_write_ptr_bad;
use nix::sys::ioctl::ioctl_num_type;
use nix::sys::socket::{socket, AddressFamily, SockFlag, SockType};
-use std::ffi::CString;
-use std::fs::File;
+use std::ffi::{CStr, CString};
+use std::fs::OpenOptions;
use std::os::fd::{AsRawFd, RawFd};
use std::slice::from_raw_parts;
+const TUNGETIFF: ioctl_num_type = 0x800454d2u32 as ioctl_num_type;
const TUNSETIFF: ioctl_num_type = 0x400454ca;
-const TUNSETPERSIST: ioctl_num_type = 0x400454cb;
-const SIOCGIFFLAGS: ioctl_num_type = 0x00008913;
const SIOCSIFFLAGS: ioctl_num_type = 0x00008914;
+ioctl_write_ptr_bad!(ioctl_tungetiff, TUNGETIFF, ifreq);
ioctl_write_ptr_bad!(ioctl_tunsetiff, TUNSETIFF, ifreq);
-ioctl_write_int_bad!(ioctl_tunsetpersist, TUNSETPERSIST);
-ioctl_write_ptr_bad!(ioctl_siocgifflags, SIOCGIFFLAGS, ifreq);
ioctl_write_ptr_bad!(ioctl_siocsifflags, SIOCSIFFLAGS, ifreq);
fn validate_ifname(ifname: &[c_char]) -> Result<()> {
@@ -44,32 +42,34 @@
Ok(())
}
-fn create_tap_interface(fd: RawFd, ifname: &[c_char]) -> Result<()> {
+fn create_tap_interface(fd: RawFd, sockfd: c_int, ifname: &[c_char]) -> Result<()> {
// SAFETY: All-zero is a valid value for the ifreq type.
let mut ifr: ifreq = unsafe { std::mem::zeroed() };
ifr.ifr_ifru.ifru_flags = (IFF_TAP | IFF_NO_PI | IFF_VNET_HDR) as c_short;
ifr.ifr_name[..ifname.len()].copy_from_slice(ifname);
- // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
- // state of this process in any way.
+ // SAFETY: It modifies the state in the kernel, not the state of this process in any way.
unsafe { ioctl_tunsetiff(fd, &ifr) }.context("Failed to ioctl TUNSETIFF")?;
- // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
- // state of this process in any way.
- unsafe { ioctl_tunsetpersist(fd, 1) }.context("Failed to ioctl TUNSETPERSIST")?;
+ // SAFETY: ifr_ifru holds ifru_flags in its union field.
+ unsafe { ifr.ifr_ifru.ifru_flags |= IFF_UP as c_short };
+ // SAFETY: It modifies the state in the kernel, not the state of this process in any way.
+ unsafe { ioctl_siocsifflags(sockfd, &ifr) }.context("Failed to ioctl SIOCSIFFLAGS")?;
Ok(())
}
-fn bring_up_interface(sockfd: c_int, ifname: &[c_char]) -> Result<()> {
+fn get_tap_ifreq(fd: RawFd) -> Result<ifreq> {
// SAFETY: All-zero is a valid value for the ifreq type.
- let mut ifr: ifreq = unsafe { std::mem::zeroed() };
- ifr.ifr_name[..ifname.len()].copy_from_slice(ifname);
- // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
- // state of this process in any way.
- unsafe { ioctl_siocgifflags(sockfd, &ifr) }.context("Failed to ioctl SIOCGIFFLAGS")?;
- // SAFETY: After calling SIOCGIFFLAGS, ifr_ifru holds ifru_flags in its union field.
- unsafe { ifr.ifr_ifru.ifru_flags |= IFF_UP as c_short };
- // SAFETY: `ioctl` is copied into the kernel. It modifies the state in the kernel, not the
- // state of this process in any way.
- unsafe { ioctl_siocsifflags(sockfd, &ifr) }.context("Failed to ioctl SIOCGIFFLAGS")?;
+ let ifr: ifreq = unsafe { std::mem::zeroed() };
+ // SAFETY: Returned `ifr` of given file descriptor is set from TUNSETIFF ioctl while executing
+ // create_tap_interface(fd, sockfd, ifname). So the variable `ifr` should be safe.
+ unsafe { ioctl_tungetiff(fd, &ifr) }.context("Failed to ioctl TUNGETIFF")?;
+ Ok(ifr)
+}
+
+fn delete_tap_interface(sockfd: c_int, ifr: &mut ifreq) -> Result<()> {
+ // SAFETY: After calling TUNGETIFF, ifr_ifru holds ifru_flags in its union field.
+ unsafe { ifr.ifr_ifru.ifru_flags &= !IFF_UP as c_short };
+ // SAFETY: It modifies the state in the kernel, not the state of this process in any way.
+ unsafe { ioctl_siocsifflags(sockfd, ifr) }.context("Failed to ioctl SIOCSIFFLAGS")?;
Ok(())
}
@@ -99,21 +99,39 @@
.context(format!("Invalid interface name: {ifname:#?}"))
.or_service_specific_exception(-1)?;
- let tunfd = File::open("/dev/tun")
+ let tunfd = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open("/dev/tun")
.context("Failed to open /dev/tun")
.or_service_specific_exception(-1)?;
- create_tap_interface(tunfd.as_raw_fd(), ifname_bytes)
- .context(format!("Failed to create TAP interface: {ifname:#?}"))
- .or_service_specific_exception(-1)?;
-
let sock = socket(AddressFamily::Inet, SockType::Datagram, SockFlag::empty(), None)
.context("Failed to create socket")
.or_service_specific_exception(-1)?;
- bring_up_interface(sock.as_raw_fd(), ifname_bytes)
- .context(format!("Failed to bring up TAP interface: {ifname:#?}"))
+ create_tap_interface(tunfd.as_raw_fd(), sock.as_raw_fd(), ifname_bytes)
+ .context(format!("Failed to create TAP interface: {ifname:#?}"))
.or_service_specific_exception(-1)?;
info!("Created TAP network interface: {ifname:#?}");
Ok(ParcelFileDescriptor::new(tunfd))
}
+
+ fn deleteTapInterface(&self, tapfd: &ParcelFileDescriptor) -> binder::Result<()> {
+ let mut tap_ifreq = get_tap_ifreq(tapfd.as_raw_fd())
+ .context("Failed to get ifreq of TAP interface")
+ .or_service_specific_exception(-1)?;
+ // SAFETY: tap_ifreq.ifr_name is null-terminated within IFNAMSIZ, validated when creating
+ // TAP interface.
+ let ifname = unsafe { CStr::from_ptr(tap_ifreq.ifr_name.as_ptr()) };
+
+ let sock = socket(AddressFamily::Inet, SockType::Datagram, SockFlag::empty(), None)
+ .context("Failed to create socket")
+ .or_service_specific_exception(-1)?;
+ delete_tap_interface(sock.as_raw_fd(), &mut tap_ifreq)
+ .context(format!("Failed to create TAP interface: {ifname:#?}"))
+ .or_service_specific_exception(-1)?;
+
+ info!("Deleted TAP network interface: {ifname:#?}");
+ Ok(())
+ }
}
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 390a60d..3c0887c 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -24,15 +24,18 @@
};
#[cfg(not(llpvm_changes))]
use anyhow::anyhow;
-use anyhow::{Context, Error};
+use anyhow::{bail, Context, Error};
use binder::{ProcessState, Strong};
use clap::{Args, Parser};
use create_idsig::command_create_idsig;
use create_partition::command_create_partition;
use run::{command_run, command_run_app, command_run_microdroid};
use serde::Serialize;
+use std::io::{self, IsTerminal};
use std::num::NonZeroU16;
+use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
+use std::process::Command;
#[derive(Args, Default)]
/// Collection of flags that are at VM level and therefore applicable to all subcommands
@@ -65,6 +68,10 @@
#[cfg(network)]
#[arg(short, long)]
network_supported: bool,
+
+ /// Boost uclamp to stablise results for benchmarks.
+ #[arg(short, long)]
+ boost_uclamp: bool,
}
impl CommonConfig {
@@ -320,6 +327,11 @@
/// Path to idsig of the APK
path: PathBuf,
},
+ /// Connect to the serial console of a VM
+ Console {
+ /// CID of the VM
+ cid: Option<i32>,
+ },
}
fn parse_debug_level(s: &str) -> Result<DebugLevel, String> {
@@ -382,6 +394,7 @@
Opt::CreateIdsig { apk, path } => {
command_create_idsig(get_service()?.as_ref(), &apk, &path)
}
+ Opt::Console { cid } => command_console(cid),
}
}
@@ -446,6 +459,21 @@
Ok(())
}
+fn command_console(cid: Option<i32>) -> Result<(), Error> {
+ if !io::stdin().is_terminal() {
+ bail!("Stdin must be a terminal (tty). Use 'adb shell -t' to force allocate tty.");
+ }
+ let mut vms = get_service()?.debugListVms().context("Failed to get list of VMs")?;
+ if let Some(cid) = cid {
+ vms.retain(|vm_info| vm_info.cid == cid);
+ }
+ let host_console_name = vms
+ .into_iter()
+ .find_map(|vm_info| vm_info.hostConsoleName)
+ .context("Failed to get VM with console")?;
+ Err(Command::new("microcom").arg(host_console_name).exec().into())
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 0c9fbb6..cb15802 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -179,6 +179,7 @@
customConfig: Some(custom_config),
osName: os_name,
hugePages: config.common.hugepages,
+ boostUclamp: config.common.boost_uclamp,
});
run(
service.as_ref(),
@@ -260,6 +261,7 @@
}
vm_config.cpuTopology = config.common.cpu_topology;
vm_config.hugePages = config.common.hugepages;
+ vm_config.boostUclamp = config.common.boost_uclamp;
run(
get_service()?.as_ref(),
&VirtualMachineConfig::RawConfig(vm_config),
diff --git a/vm_payload/Android.bp b/vm_payload/Android.bp
index 229f533..cf2a002 100644
--- a/vm_payload/Android.bp
+++ b/vm_payload/Android.bp
@@ -39,8 +39,8 @@
visibility: [":__subpackages__"],
}
-// Rust wrappers round the C API for Rust clients.
-// (Yes, this involves going Rust -> C -> Rust.)
+// Access to the C API for Rust code.
+// This shouldn't be used directly - prefer libvm_payload_rs (below)
rust_bindgen {
name: "libvm_payload_bindgen",
wrapper_src: "include-restricted/vm_payload_restricted.h",
@@ -51,15 +51,31 @@
bindgen_flags: [
"--default-enum-style rust",
],
- visibility: [
- "//packages/modules/Virtualization/compos",
- "//packages/modules/Virtualization/service_vm:__subpackages__",
- ],
shared_libs: [
"libvm_payload#current",
],
}
+// Wrapper library for the raw C API for use by Rust clients.
+// (Yes, this involves going Rust -> C -> Rust.)
+// This is not a stable API - we may change it in subsequent versions.
+// But it is made available as an rlib so it is linked into any
+// code using it, leaving only dependencies on stable APIs.
+// So code built with it should run unchanged on future versions.
+rust_library_rlib {
+ name: "libvm_payload_rs",
+ crate_name: "vm_payload",
+ defaults: ["avf_build_flags_rust"],
+ srcs: ["wrapper/lib.rs"],
+ rustlibs: [
+ "libbinder_rs",
+ "libstatic_assertions",
+ "libvm_payload_bindgen",
+ ],
+ apex_available: ["com.android.compos"],
+ visibility: ["//visibility:public"],
+}
+
// Shared library for clients to link against.
cc_library_shared {
name: "libvm_payload",
diff --git a/vm_payload/README.md b/vm_payload/README.md
index 4b1e6f3..66fd532 100644
--- a/vm_payload/README.md
+++ b/vm_payload/README.md
@@ -70,3 +70,16 @@
See [AIDL
backends](https://source.android.com/docs/core/architecture/aidl/aidl-backends)
for information on using AIDL with the NDK Binder from C++.
+
+## Rust
+
+A Rust wrapper library for the VM Payload API is available (as an rlib) for VM
+payloads written in Rust.
+
+This wrapper is not guaranteed to be stable; we may change it in future
+versions. But payload code built using it will depend only on the C VM Payload
+API and the NDK APIs that are available to the payload, so should run unchanged
+on future versions.
+
+See [wrapper/lib.rs](wrapper/lib.rs) and `libvm_payload_rs` in
+[Android.bp](Android.bp).
diff --git a/vm_payload/wrapper/attestation.rs b/vm_payload/wrapper/attestation.rs
new file mode 100644
index 0000000..e0055d5
--- /dev/null
+++ b/vm_payload/wrapper/attestation.rs
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2024 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 std::error::Error;
+use std::ffi::{c_void, CStr};
+use std::fmt::{self, Display};
+use std::iter::FusedIterator;
+use std::ptr::{self, NonNull};
+
+use vm_payload_bindgen::{
+ AVmAttestationResult, AVmAttestationResult_free, AVmAttestationResult_getCertificateAt,
+ AVmAttestationResult_getCertificateCount, AVmAttestationResult_getPrivateKey,
+ AVmAttestationResult_sign, AVmAttestationStatus, AVmAttestationStatus_toString,
+ AVmPayload_requestAttestation, AVmPayload_requestAttestationForTesting,
+};
+
+/// Holds the result of a successful Virtual Machine attestation request.
+/// See [`request_attestation`].
+#[derive(Debug)]
+pub struct AttestationResult {
+ result: NonNull<AVmAttestationResult>,
+}
+
+/// Error type that can be returned from an unsuccessful Virtual Machine attestation request.
+/// See [`request_attestation`].
+#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
+pub enum AttestationError {
+ /// The challenge size was not between 0 and 64 bytes (inclusive).
+ InvalidChallenge,
+ /// The attempt to attest the VM failed. A subsequent request may succeed.
+ AttestationFailed,
+ /// VM attestation is not supported in the current environment.
+ AttestationUnsupported,
+}
+
+impl Error for AttestationError {}
+
+impl Display for AttestationError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+ let status = match self {
+ Self::InvalidChallenge => AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE,
+ Self::AttestationFailed => AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED,
+ Self::AttestationUnsupported => AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED,
+ };
+ // SAFETY: AVmAttestationStatus_toString always returns a non-null pointer to a
+ // nul-terminated C string with static lifetime (which is valid UTF-8).
+ let c_str = unsafe { CStr::from_ptr(AVmAttestationStatus_toString(status)) };
+ let str = c_str.to_str().expect("Invalid UTF-8 for AVmAttestationStatus");
+ f.write_str(str)
+ }
+}
+
+impl Drop for AttestationResult {
+ fn drop(&mut self) {
+ let ptr = self.result.as_ptr();
+
+ // SAFETY: The `result` field is private, and only populated with a successful call to
+ // `AVmPayload_requestAttestation`, and not freed elsewhere.
+ unsafe { AVmAttestationResult_free(ptr) };
+ }
+}
+
+// SAFETY: The API functions that accept the `AVmAttestationResult` pointer are all safe to call
+// from any thread, including `AVmAttestationResult_free` which is called only on drop.
+unsafe impl Send for AttestationResult {}
+
+// SAFETY: There is no interior mutation here; any future functions that might mutate the data would
+// require a non-const pointer and hence need `&mut self` here. The only existing such function is
+// `AVmAttestationResult_free` where we take a mutable reference guaranteeing no other references
+// exist. The raw API functions are safe to call from any thread.
+unsafe impl Sync for AttestationResult {}
+
+/// Requests the remote attestation of this VM.
+///
+/// On success the supplied [`challenge`] will be included in the certificate chain accessible from
+/// the [`AttestationResult`]; this can be used as proof of the freshness of the attestation.
+///
+/// The challenge should be no more than 64 bytes long or the request will fail.
+pub fn request_attestation(challenge: &[u8]) -> Result<AttestationResult, AttestationError> {
+ let mut result: *mut AVmAttestationResult = ptr::null_mut();
+ // SAFETY: We only read the challenge within its bounds and the function does not retain any
+ // reference to it.
+ let status = unsafe {
+ AVmPayload_requestAttestation(
+ challenge.as_ptr() as *const c_void,
+ challenge.len(),
+ &mut result,
+ )
+ };
+ AttestationResult::new(status, result)
+}
+
+/// A variant of [`request_attestation`] used for testing purposes. This should not be used by
+/// normal VMs, and is not available to app owned VMs.
+pub fn request_attestation_for_testing(
+ challenge: &[u8],
+) -> Result<AttestationResult, AttestationError> {
+ let mut result: *mut AVmAttestationResult = ptr::null_mut();
+ // SAFETY: We only read the challenge within its bounds and the function does not retain any
+ // reference to it.
+ let status = unsafe {
+ AVmPayload_requestAttestationForTesting(
+ challenge.as_ptr() as *const c_void,
+ challenge.len(),
+ &mut result,
+ )
+ };
+ AttestationResult::new(status, result)
+}
+
+impl AttestationResult {
+ fn new(
+ status: AVmAttestationStatus,
+ result: *mut AVmAttestationResult,
+ ) -> Result<AttestationResult, AttestationError> {
+ match status {
+ AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE => {
+ Err(AttestationError::InvalidChallenge)
+ }
+ AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED => {
+ Err(AttestationError::AttestationFailed)
+ }
+ AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => {
+ Err(AttestationError::AttestationUnsupported)
+ }
+ AVmAttestationStatus::ATTESTATION_OK => {
+ let result = NonNull::new(result)
+ .expect("Attestation succeeded but the attestation result is null");
+ Ok(AttestationResult { result })
+ }
+ }
+ }
+
+ fn as_const_ptr(&self) -> *const AVmAttestationResult {
+ self.result.as_ptr().cast_const()
+ }
+
+ /// Returns the attested private key. This is the ECDSA P-256 private key corresponding to the
+ /// public key described by the leaf certificate in the attested
+ /// [certificate chain](AttestationResult::certificate_chain). It is a DER-encoded
+ /// `ECPrivateKey` structure as specified in
+ /// [RFC 5915 s3](https://datatracker.ietf.org/doc/html/rfc5915#section-3).
+ ///
+ /// Note: The [`sign_message`](AttestationResult::sign_message) method allows signing with the
+ /// key without retrieving it.
+ pub fn private_key(&self) -> Vec<u8> {
+ let ptr = self.as_const_ptr();
+
+ let size =
+ // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function
+ // writes no data since we pass a zero size, and null is explicitly allowed for the
+ // destination in that case.
+ unsafe { AVmAttestationResult_getPrivateKey(ptr, ptr::null_mut(), 0) };
+
+ let mut private_key = vec![0u8; size];
+ // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function only
+ // writes within the bounds of `private_key`, which we just allocated so cannot be aliased.
+ let size = unsafe {
+ AVmAttestationResult_getPrivateKey(
+ ptr,
+ private_key.as_mut_ptr() as *mut c_void,
+ private_key.len(),
+ )
+ };
+ assert_eq!(size, private_key.len());
+ private_key
+ }
+
+ /// Signs the given message using the attested private key. The signature uses ECDSA P-256; the
+ /// message is first hashed with SHA-256 and then it is signed with the attested EC P-256
+ /// [private key](AttestationResult::private_key).
+ ///
+ /// The signature is a DER-encoded `ECDSASignature`` structure as described in
+ /// [RFC 6979](https://datatracker.ietf.org/doc/html/rfc6979).
+ pub fn sign_message(&self, message: &[u8]) -> Vec<u8> {
+ let ptr = self.as_const_ptr();
+
+ // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function
+ // writes no data since we pass a zero size, and null is explicitly allowed for the
+ // destination in that case.
+ let size = unsafe {
+ AVmAttestationResult_sign(
+ ptr,
+ message.as_ptr() as *const c_void,
+ message.len(),
+ ptr::null_mut(),
+ 0,
+ )
+ };
+
+ let mut signature = vec![0u8; size];
+ // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function only
+ // writes within the bounds of `signature`, which we just allocated so cannot be aliased.
+ let size = unsafe {
+ AVmAttestationResult_sign(
+ ptr,
+ message.as_ptr() as *const c_void,
+ message.len(),
+ signature.as_mut_ptr() as *mut c_void,
+ signature.len(),
+ )
+ };
+ assert!(size <= signature.len());
+ signature.truncate(size);
+ signature
+ }
+
+ /// Returns an iterator over the certificates forming the certificate chain for the VM, and its
+ /// public key, obtained by the attestation process.
+ ///
+ /// The certificate chain consists of a sequence of DER-encoded X.509 certificates that form
+ /// the attestation key's certificate chain. It starts with the leaf certificate covering the
+ /// attested public key and ends with the root certificate.
+ pub fn certificate_chain(&self) -> CertIterator {
+ // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid.
+ let count = unsafe { AVmAttestationResult_getCertificateCount(self.as_const_ptr()) };
+
+ CertIterator { result: self, count, current: 0 }
+ }
+
+ fn certificate(&self, index: usize) -> Vec<u8> {
+ let ptr = self.as_const_ptr();
+
+ let size =
+ // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function
+ // writes no data since we pass a zero size, and null is explicitly allowed for the
+ // destination in that case. The function will panic if `index` is out of range (which
+ // is safe).
+ unsafe { AVmAttestationResult_getCertificateAt(ptr, index, ptr::null_mut(), 0) };
+
+ let mut cert = vec![0u8; size];
+ // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function only
+ // writes within the bounds of `cert`, which we just allocated so cannot be aliased.
+ let size = unsafe {
+ AVmAttestationResult_getCertificateAt(
+ ptr,
+ index,
+ cert.as_mut_ptr() as *mut c_void,
+ cert.len(),
+ )
+ };
+ assert_eq!(size, cert.len());
+ cert
+ }
+}
+
+/// An iterator over the DER-encoded X.509 certificates containin in an [`AttestationResult`].
+/// See [`certificate_chain`](AttestationResult::certificate_chain) for more details.
+pub struct CertIterator<'a> {
+ result: &'a AttestationResult,
+ count: usize,
+ current: usize, // Invariant: current <= count
+}
+
+impl<'a> Iterator for CertIterator<'a> {
+ type Item = Vec<u8>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.current < self.count {
+ let cert = self.result.certificate(self.current);
+ self.current += 1;
+ Some(cert)
+ } else {
+ None
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ let size = self.count - self.current;
+ (size, Some(size))
+ }
+}
+
+impl<'a> ExactSizeIterator for CertIterator<'a> {}
+impl<'a> FusedIterator for CertIterator<'a> {}
diff --git a/vm_payload/wrapper/lib.rs b/vm_payload/wrapper/lib.rs
new file mode 100644
index 0000000..d3f03d7
--- /dev/null
+++ b/vm_payload/wrapper/lib.rs
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2024 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.
+ */
+
+//! Rust wrapper for the VM Payload API, allowing virtual machine payload code to be written in
+//! Rust. This wraps the raw C API, accessed via bindgen, into a more idiomatic Rust interface.
+//!
+//! See `https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/vm_payload/README.md`
+//! for more information on the VM Payload API.
+
+mod attestation;
+
+pub use attestation::{request_attestation, AttestationError, AttestationResult};
+use binder::unstable_api::AsNative;
+use binder::{FromIBinder, Strong};
+use std::ffi::{c_void, CStr, OsStr};
+use std::os::unix::ffi::OsStrExt;
+use std::path::Path;
+use std::ptr;
+use vm_payload_bindgen::{
+ AIBinder, AVmPayload_getApkContentsPath, AVmPayload_getEncryptedStoragePath,
+ AVmPayload_getVmInstanceSecret, AVmPayload_notifyPayloadReady, AVmPayload_runVsockRpcServer,
+};
+
+/// The functions declared here are restricted to VMs created with a config file;
+/// they will fail, or panic, if called in other VMs. The ability to create such VMs
+/// requires the android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission, and is
+/// therefore not available to privileged or third party apps.
+///
+/// These functions can be used by tests, if the permission is granted via shell.
+pub mod restricted {
+ pub use crate::attestation::request_attestation_for_testing;
+}
+
+/// Marks the main function of the VM payload.
+///
+/// When the VM is run, this function is called. If it returns, the VM ends normally with a 0 exit
+/// code.
+///
+/// Example:
+///
+/// ```rust
+/// use log::info;
+///
+/// vm_payload::main!(vm_main);
+///
+/// fn vm_main() {
+/// android_logger::init_once(
+/// android_logger::Config::default()
+/// .with_tag("example_vm_payload")
+/// .with_max_level(log::LevelFilter::Info),
+/// );
+/// info!("Hello world");
+/// }
+/// ```
+#[macro_export]
+macro_rules! main {
+ ($name:path) => {
+ // Export a symbol with a name matching the extern declaration below.
+ #[export_name = "rust_main"]
+ fn __main() {
+ // Ensure that the main function provided by the application has the correct type.
+ $name()
+ }
+ };
+}
+
+// This is the real C entry point for the VM; we just forward to the Rust entry point.
+#[allow(non_snake_case)]
+#[no_mangle]
+extern "C" fn AVmPayload_main() {
+ extern "Rust" {
+ fn rust_main();
+ }
+
+ // SAFETY: rust_main is provided by the application using the `main!` macro above, which makes
+ // sure it has the right type.
+ unsafe { rust_main() }
+}
+
+/// Notifies the host that the payload is ready.
+///
+/// If the host app has set a `VirtualMachineCallback` for the VM, its
+/// `onPayloadReady` method will be called.
+///
+/// Note that subsequent calls to this function after the first have no effect;
+/// `onPayloadReady` is never called more than once.
+pub fn notify_payload_ready() {
+ // SAFETY: Invokes a method from the bindgen library `vm_payload_bindgen` which is safe to
+ // call at any time.
+ unsafe { AVmPayload_notifyPayloadReady() };
+}
+
+/// Runs a binder RPC server, serving the supplied binder service implementation on the given vsock
+/// port.
+///
+/// If and when the server is ready for connections (i.e. it is listening on the port),
+/// [`notify_payload_ready`] is called to notify the host that the server is ready. This is
+/// appropriate for VM payloads that serve a single binder service - which is common.
+///
+/// Note that this function does not return. The calling thread joins the binder
+/// thread pool to handle incoming messages.
+pub fn run_single_vsock_service<T>(service: Strong<T>, port: u32) -> !
+where
+ T: FromIBinder + ?Sized,
+{
+ extern "C" fn on_ready(_param: *mut c_void) {
+ notify_payload_ready();
+ }
+
+ let mut service = service.as_binder();
+ // The cast here is needed because the compiler doesn't know that our vm_payload_bindgen
+ // AIBinder is the same type as binder_ndk_sys::AIBinder.
+ let service = service.as_native_mut() as *mut AIBinder;
+ let param = ptr::null_mut();
+ // SAFETY: We have a strong reference to the service, so the raw pointer remains valid. It is
+ // safe for on_ready to be invoked at any time, with any parameter.
+ unsafe { AVmPayload_runVsockRpcServer(service, port, Some(on_ready), param) }
+}
+
+/// Gets the path to the contents of the APK containing the VM payload. It is a directory, under
+/// which are the unzipped contents of the APK containing the payload, all read-only
+/// but accessible to the payload.
+pub fn apk_contents_path() -> &'static Path {
+ // SAFETY: AVmPayload_getApkContentsPath always returns a non-null pointer to a
+ // nul-terminated C string with static lifetime.
+ let c_str = unsafe { CStr::from_ptr(AVmPayload_getApkContentsPath()) };
+ Path::new(OsStr::from_bytes(c_str.to_bytes()))
+}
+
+/// Gets the path to the encrypted persistent storage for the VM, if any. This is
+/// a directory under which any files or directories created will be stored on
+/// behalf of the VM by the host app. All data is encrypted using a key known
+/// only to the VM, so the host cannot decrypt it, but may delete it.
+///
+/// Returns `None` if no encrypted storage was requested in the VM configuration.
+pub fn encrypted_storage_path() -> Option<&'static Path> {
+ // SAFETY: AVmPayload_getEncryptedStoragePath returns either null or a pointer to a
+ // nul-terminated C string with static lifetime.
+ let ptr = unsafe { AVmPayload_getEncryptedStoragePath() };
+ if ptr.is_null() {
+ None
+ } else {
+ // SAFETY: We know the pointer is not null, and so it is a valid C string.
+ let c_str = unsafe { CStr::from_ptr(ptr) };
+ Some(Path::new(OsStr::from_bytes(c_str.to_bytes())))
+ }
+}
+
+/// Retrieves all or part of a 32-byte secret that is bound to this unique VM
+/// instance and the supplied identifier. The secret can be used e.g. as an
+/// encryption key.
+///
+/// Every VM has a secret that is derived from a device-specific value known to
+/// the hypervisor, the code that runs in the VM and its non-modifiable
+/// configuration; it is not made available to the host OS.
+///
+/// This function performs a further derivation from the VM secret and the
+/// supplied identifier. As long as the VM identity doesn't change the same value
+/// will be returned for the same identifier, even if the VM is stopped &
+/// restarted or the device rebooted.
+///
+/// If multiple secrets are required for different purposes, a different
+/// identifier should be used for each. The identifiers otherwise are arbitrary
+/// byte sequences and do not need to be kept secret; typically they are
+/// hardcoded in the calling code.
+///
+/// The secret is returned in [`secret`], truncated to its size, which must be between
+/// 1 and 32 bytes (inclusive) or the function will panic.
+pub fn get_vm_instance_secret(identifier: &[u8], secret: &mut [u8]) {
+ let secret_size = secret.len();
+ assert!((1..=32).contains(&secret_size), "VM instance secrets can be up to 32 bytes long");
+
+ // SAFETY: The function only reads from `[identifier]` within its bounds, and only writes to
+ // `[secret]` within its bounds. Neither reference is retained, and we know neither is null.
+ unsafe {
+ AVmPayload_getVmInstanceSecret(
+ identifier.as_ptr() as *const c_void,
+ identifier.len(),
+ secret.as_mut_ptr() as *mut c_void,
+ secret_size,
+ )
+ }
+}
diff --git a/vmbase/Android.bp b/vmbase/Android.bp
index f01e8aa..ee12e85 100644
--- a/vmbase/Android.bp
+++ b/vmbase/Android.bp
@@ -57,6 +57,8 @@
hwaddress: false,
},
native_coverage: false,
+ // TODO(b/346974429): Workaround pvmfw failure when enabling full LTO
+ lto_O0: true,
}
// Used by cc_binary when producing the ELF of a vmbase-based binary.
diff --git a/vmbase/src/uart.rs b/vmbase/src/uart.rs
index 09d747f..e35555d 100644
--- a/vmbase/src/uart.rs
+++ b/vmbase/src/uart.rs
@@ -16,7 +16,6 @@
//! provided by crosvm, and won't work with real hardware.
use core::fmt::{self, Write};
-use core::ptr::write_volatile;
/// Minimal driver for an 8250 UART. This only implements enough to work with the emulated 8250
/// provided by crosvm, and won't work with real hardware.
@@ -41,7 +40,11 @@
// SAFETY: We know that the base address points to the control registers of a UART device
// which is appropriately mapped.
unsafe {
- write_volatile(self.base_address, byte);
+ core::arch::asm!(
+ "strb {value:w}, [{ptr}]",
+ value = in(reg) byte,
+ ptr = in(reg) self.base_address,
+ );
}
}
}
diff --git a/vmlauncher_app/AndroidManifest.xml b/vmlauncher_app/AndroidManifest.xml
index d800ec7..bae3227 100644
--- a/vmlauncher_app/AndroidManifest.xml
+++ b/vmlauncher_app/AndroidManifest.xml
@@ -7,19 +7,27 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.software.virtualization_framework" android:required="true" />
<application
- android:label="VmLauncherApp"
- android:networkSecurityConfig="@xml/network_security_config">
+ android:label="VmLauncherApp">
<activity android:name=".MainActivity"
- android:enabled="false"
android:screenOrientation="landscape"
android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
android:theme="@style/MyTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
+ <action android:name="android.virtualization.VM_LAUNCHER" />
+ <category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
+ <activity-alias android:name=".MainActivityAlias"
+ android:targetActivity="com.android.virtualization.vmlauncher.MainActivity"
+ android:exported="true"
+ android:enabled="false">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity-alias>
</application>
</manifest>
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 5355313..c2f218a 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -32,6 +32,7 @@
import android.system.virtualmachine.VirtualMachineConfig;
import android.system.virtualmachine.VirtualMachineCustomImageConfig;
import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.GpuConfig;
import android.system.virtualmachine.VirtualMachineException;
import android.system.virtualmachine.VirtualMachineManager;
import android.util.DisplayMetrics;
@@ -63,6 +64,7 @@
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -125,6 +127,47 @@
if (json.has("console_input_device")) {
configBuilder.setConsoleInputDevice(json.getString("console_input_device"));
}
+ if (json.has("gpu")) {
+ JSONObject gpuJson = json.getJSONObject("gpu");
+
+ GpuConfig.Builder gpuConfigBuilder = new GpuConfig.Builder();
+
+ if (gpuJson.has("backend")) {
+ gpuConfigBuilder.setBackend(gpuJson.getString("backend"));
+ }
+ if (gpuJson.has("context_types")) {
+ ArrayList<String> contextTypes = new ArrayList<String>();
+ JSONArray contextTypesJson = gpuJson.getJSONArray("context_types");
+ for (int i = 0; i < contextTypesJson.length(); i++) {
+ contextTypes.add(contextTypesJson.getString(i));
+ }
+ gpuConfigBuilder.setContextTypes(contextTypes.toArray(new String[0]));
+ }
+ if (gpuJson.has("pci_address")) {
+ gpuConfigBuilder.setPciAddress(gpuJson.getString("pci_address"));
+ }
+ if (gpuJson.has("renderer_features")) {
+ gpuConfigBuilder.setRendererFeatures(gpuJson.getString("renderer_features"));
+ }
+ if (gpuJson.has("renderer_use_egl")) {
+ gpuConfigBuilder.setRendererUseEgl(gpuJson.getBoolean("renderer_use_egl"));
+ }
+ if (gpuJson.has("renderer_use_gles")) {
+ gpuConfigBuilder.setRendererUseGles(gpuJson.getBoolean("renderer_use_gles"));
+ }
+ if (gpuJson.has("renderer_use_glx")) {
+ gpuConfigBuilder.setRendererUseGlx(gpuJson.getBoolean("renderer_use_glx"));
+ }
+ if (gpuJson.has("renderer_use_surfaceless")) {
+ gpuConfigBuilder.setRendererUseSurfaceless(
+ gpuJson.getBoolean("renderer_use_surfaceless"));
+ }
+ if (gpuJson.has("renderer_use_vulkan")) {
+ gpuConfigBuilder.setRendererUseVulkan(
+ gpuJson.getBoolean("renderer_use_vulkan"));
+ }
+ customImageConfigBuilder.setGpuConfig(gpuConfigBuilder.build());
+ }
configBuilder.setMemoryBytes(8L * 1024 * 1024 * 1024 /* 8 GB */);
WindowMetrics windowMetrics = getWindowManager().getCurrentWindowMetrics();
@@ -145,6 +188,7 @@
customImageConfigBuilder.useTouch(true);
customImageConfigBuilder.useKeyboard(true);
customImageConfigBuilder.useMouse(true);
+ customImageConfigBuilder.useNetwork(true);
configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
diff --git a/vmlauncher_app/res/xml/network_security_config.xml b/vmlauncher_app/res/xml/network_security_config.xml
deleted file mode 100644
index f27fa56..0000000
--- a/vmlauncher_app/res/xml/network_security_config.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2024 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.
- -->
-
-<network-security-config>
- <domain-config cleartextTrafficPermitted="true">
- <domain includeSubdomains="true">localhost</domain>
- </domain-config>
-</network-security-config>