Merge changes from topic "build_libforwarder" into main

* changes:
  Make libforwarder buildable
  Introduce libforwarder
diff --git a/OWNERS b/OWNERS
index 40c709f..717a4db 100644
--- a/OWNERS
+++ b/OWNERS
@@ -28,3 +28,10 @@
 tabba@google.com
 vdonnefort@google.com
 victorhsieh@google.com
+
+# Ferrochrome
+per-file android/FerrochromeApp/**=jiyong@google.com,jeongik@google.com
+per-file android/LinuxInstaller/**=jiyong@google.com,jeongik@google.com
+per-file android/TerminalApp/**=jiyong@google.com,jeongik@google.com
+per-file android/VmLauncherApp/**=jiyong@google.com,jeongik@google.com
+per-file libs/vm_launcher_lib/**=jiyong@google.com,jeongik@google.com
diff --git a/android/LinuxInstaller/linux_image_builder/ttyd.service b/android/LinuxInstaller/linux_image_builder/ttyd.service
index 3a8f181..f71557d 100644
--- a/android/LinuxInstaller/linux_image_builder/ttyd.service
+++ b/android/LinuxInstaller/linux_image_builder/ttyd.service
@@ -3,7 +3,7 @@
 After=syslog.target
 After=network.target
 [Service]
-ExecStart=/usr/local/bin/ttyd -W login
+ExecStart=/usr/local/bin/ttyd -W screen -aAxR -S main login
 Type=simple
 Restart=always
 User=root
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 3ae014e..1a7c581 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -9,8 +9,10 @@
     static_libs: [
         "vm_launcher_lib",
     ],
-    sdk_version: "system_current",
+    platform_apis: true,
+    privileged: true,
     optimize: {
+        proguard_flags_files: ["proguard.flags"],
         shrink_resources: true,
     },
     apex_available: [
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index c92da67..e338c49 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -2,9 +2,13 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.virtualization.terminal" >
 
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
 
+    <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
     <application
 	android:label="@string/app_name"
         android:icon="@mipmap/ic_launcher"
@@ -27,6 +31,20 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+
+        <service
+            android:name="com.android.virtualization.vmlauncher.VmLauncherService"
+            android:enabled="true"
+            android:exported="false"
+            android:foregroundServiceType="specialUse">
+            <property
+                android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+                android:value="Run VM instances" />
+            <intent-filter>
+                <action android:name="android.virtualization.START_VM_LAUNCHER_SERVICE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </service>
     </application>
 
 </manifest>
diff --git a/android/TerminalApp/proguard.flags b/android/TerminalApp/proguard.flags
new file mode 100644
index 0000000..13ec24e
--- /dev/null
+++ b/android/TerminalApp/proguard.flags
@@ -0,0 +1,7 @@
+# Keep the no-args constructor of the deserialized class
+-keepclassmembers class com.android.virtualization.vmlauncher.ConfigJson {
+  <init>();
+}
+-keepclassmembers class com.android.virtualization.vmlauncher.ConfigJson$* {
+  <init>();
+}
diff --git a/android/VmLauncherApp/Android.bp b/android/VmLauncherApp/Android.bp
index 7dd2473..2e8cc93 100644
--- a/android/VmLauncherApp/Android.bp
+++ b/android/VmLauncherApp/Android.bp
@@ -11,7 +11,7 @@
         "android.system.virtualizationservice_internal-java",
         // TODO(b/331708504): will be removed when AVF framework handles surface
         "libcrosvm_android_display_service-java",
-        "gson",
+        "vm_launcher_lib",
     ],
     libs: [
         "framework-virtualization.impl",
diff --git a/android/VmLauncherApp/AndroidManifest.xml b/android/VmLauncherApp/AndroidManifest.xml
index 583fce7..4fb4b5c 100644
--- a/android/VmLauncherApp/AndroidManifest.xml
+++ b/android/VmLauncherApp/AndroidManifest.xml
@@ -6,8 +6,6 @@
     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
     <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
 
     <permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
@@ -28,20 +26,6 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-        <service
-            android:name=".VmLauncherService"
-            android:enabled="true"
-            android:exported="true"
-            android:permission="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
-            android:foregroundServiceType="specialUse">
-            <property
-                android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
-                android:value="Run VM instances" />
-            <intent-filter>
-                <action android:name="android.virtualization.START_VM_LAUNCHER_SERVICE" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-        </service>
 
     </application>
 
diff --git a/android/fd_server/Android.bp b/android/fd_server/Android.bp
index b02c104..748c660 100644
--- a/android/fd_server/Android.bp
+++ b/android/fd_server/Android.bp
@@ -18,6 +18,7 @@
         "liblog_rust",
         "libnix",
         "librpcbinder_rs",
+        "librustutils",
     ],
     prefer_rlib: true,
     apex_available: ["com.android.virt"],
@@ -39,6 +40,7 @@
         "liblog_rust",
         "libnix",
         "librpcbinder_rs",
+        "librustutils",
     ],
     prefer_rlib: true,
     test_suites: ["general-tests"],
diff --git a/android/fd_server/src/main.rs b/android/fd_server/src/main.rs
index 26315bf..07f0896 100644
--- a/android/fd_server/src/main.rs
+++ b/android/fd_server/src/main.rs
@@ -29,9 +29,10 @@
 use log::debug;
 use nix::sys::stat::{umask, Mode};
 use rpcbinder::RpcServer;
+use rustutils::inherited_fd::take_fd_ownership;
 use std::collections::BTreeMap;
 use std::fs::File;
-use std::os::unix::io::{FromRawFd, OwnedFd};
+use std::os::unix::io::OwnedFd;
 
 use aidl::{FdConfig, FdService};
 use authfs_fsverity_metadata::parse_fsverity_metadata;
@@ -39,20 +40,6 @@
 // TODO(b/259920193): support dynamic port for multiple fd_server instances
 const RPC_SERVICE_PORT: u32 = 3264;
 
-fn is_fd_valid(fd: i32) -> bool {
-    // SAFETY: a query-only syscall
-    let retval = unsafe { libc::fcntl(fd, libc::F_GETFD) };
-    retval >= 0
-}
-
-fn fd_to_owned<T: FromRawFd>(fd: i32) -> Result<T> {
-    if !is_fd_valid(fd) {
-        bail!("Bad FD: {}", fd);
-    }
-    // SAFETY: The caller is supposed to provide valid FDs to this process.
-    Ok(unsafe { T::from_raw_fd(fd) })
-}
-
 fn parse_arg_ro_fds(arg: &str) -> Result<(i32, FdConfig)> {
     let result: Result<Vec<i32>, _> = arg.split(':').map(|x| x.parse::<i32>()).collect();
     let fds = result?;
@@ -62,13 +49,13 @@
     Ok((
         fds[0],
         FdConfig::Readonly {
-            file: fd_to_owned(fds[0])?,
+            file: take_fd_ownership(fds[0])?.into(),
             // Alternative metadata source, if provided
             alt_metadata: fds
                 .get(1)
-                .map(|fd| fd_to_owned(*fd))
+                .map(|fd| take_fd_ownership(*fd))
                 .transpose()?
-                .and_then(|f| parse_fsverity_metadata(f).ok()),
+                .and_then(|f| parse_fsverity_metadata(f.into()).ok()),
         },
     ))
 }
@@ -105,23 +92,26 @@
         fd_pool.insert(fd, config);
     }
     for fd in args.rw_fds {
-        let file = fd_to_owned::<File>(fd)?;
+        let file: File = take_fd_ownership(fd)?.into();
         if file.metadata()?.len() > 0 {
             bail!("File is expected to be empty");
         }
         fd_pool.insert(fd, FdConfig::ReadWrite(file));
     }
     for fd in args.ro_dirs {
-        fd_pool.insert(fd, FdConfig::InputDir(fd_to_owned(fd)?));
+        fd_pool.insert(fd, FdConfig::InputDir(take_fd_ownership(fd)?));
     }
     for fd in args.rw_dirs {
-        fd_pool.insert(fd, FdConfig::OutputDir(fd_to_owned(fd)?));
+        fd_pool.insert(fd, FdConfig::OutputDir(take_fd_ownership(fd)?));
     }
-    let ready_fd = args.ready_fd.map(fd_to_owned).transpose()?;
+    let ready_fd = args.ready_fd.map(take_fd_ownership).transpose()?;
     Ok((fd_pool, ready_fd))
 }
 
 fn main() -> Result<()> {
+    // SAFETY: nobody has taken ownership of the inherited FDs yet.
+    unsafe { rustutils::inherited_fd::init_once()? };
+
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag("fd_server")
diff --git a/android/virtmgr/Android.bp b/android/virtmgr/Android.bp
index 0148ff6..d0d7915 100644
--- a/android/virtmgr/Android.bp
+++ b/android/virtmgr/Android.bp
@@ -34,7 +34,6 @@
         "libapkverify",
         "libavf_features",
         "libavflog",
-        "libbase_rust",
         "libbinder_rs",
         "libcfg_if",
         "libclap",
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index 0f41932..b2be736 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -57,7 +57,6 @@
 use rpcbinder::RpcServer;
 
 /// external/crosvm
-use base::UnixSeqpacketListener;
 use vm_control::{BalloonControlCommand, VmRequest, VmResponse};
 
 const CROSVM_PATH: &str = "/apex/com.android.virt/bin/crosvm";
@@ -1057,8 +1056,8 @@
         command.arg(add_preserved_fd(&mut preserved_fds, kernel));
     }
 
-    let control_sock = UnixSeqpacketListener::bind(crosvm_control_socket_path)
-        .context("failed to create control server")?;
+    let control_sock = create_crosvm_control_listener(crosvm_control_socket_path)
+        .context("failed to create control listener")?;
     command.arg("--socket").arg(add_preserved_fd(&mut preserved_fds, control_sock));
 
     if let Some(dt_overlay) = config.device_tree_overlay {
@@ -1272,3 +1271,22 @@
     let (read_fd, write_fd) = pipe2(OFlag::O_CLOEXEC)?;
     Ok((read_fd.into(), write_fd.into()))
 }
+
+/// Creates and binds a unix seqpacket listening socket to be passed as crosvm's `--socket`
+/// argument. See `UnixSeqpacketListener::bind` in crosvm's code for reference.
+fn create_crosvm_control_listener(crosvm_control_socket_path: &Path) -> Result<OwnedFd> {
+    use nix::sys::socket;
+    let fd = socket::socket(
+        socket::AddressFamily::Unix,
+        socket::SockType::SeqPacket,
+        socket::SockFlag::empty(),
+        None,
+    )
+    .context("socket failed")?;
+    socket::bind(fd.as_raw_fd(), &socket::UnixAddr::new(crosvm_control_socket_path)?)
+        .context("bind failed")?;
+    // The exact backlog size isn't imporant. crosvm uses 128 internally. We use 127 here
+    // because of a `nix` bug.
+    socket::listen(&fd, socket::Backlog::new(127).unwrap()).context("listen failed")?;
+    Ok(fd)
+}
diff --git a/android/virtmgr/src/main.rs b/android/virtmgr/src/main.rs
index 67e7282..1625009 100644
--- a/android/virtmgr/src/main.rs
+++ b/android/virtmgr/src/main.rs
@@ -25,15 +25,15 @@
 
 use crate::aidl::{GLOBAL_SERVICE, VirtualizationService};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::BnVirtualizationService;
-use anyhow::{bail, Context, Result};
+use anyhow::{bail, Result};
 use binder::{BinderFeatures, ProcessState};
 use log::{info, LevelFilter};
 use rpcbinder::{FileDescriptorTransportMode, RpcServer};
-use std::os::unix::io::{AsFd, FromRawFd, OwnedFd, RawFd};
+use std::os::unix::io::{AsFd, RawFd};
 use std::sync::LazyLock;
 use clap::Parser;
-use nix::fcntl::{fcntl, F_GETFD, F_SETFD, FdFlag};
 use nix::unistd::{write, Pid, Uid};
+use rustutils::inherited_fd::take_fd_ownership;
 use std::os::unix::raw::{pid_t, uid_t};
 
 const LOG_TAG: &str = "virtmgr";
@@ -71,32 +71,6 @@
     ready_fd: RawFd,
 }
 
-fn take_fd_ownership(raw_fd: RawFd, owned_fds: &mut Vec<RawFd>) -> Result<OwnedFd, anyhow::Error> {
-    // Basic check that the integer value does correspond to a file descriptor.
-    fcntl(raw_fd, F_GETFD).with_context(|| format!("Invalid file descriptor {raw_fd}"))?;
-
-    // The file descriptor had CLOEXEC disabled to be inherited from the parent.
-    // Re-enable it to make sure it is not accidentally inherited further.
-    fcntl(raw_fd, F_SETFD(FdFlag::FD_CLOEXEC))
-        .with_context(|| format!("Could not set CLOEXEC on file descriptor {raw_fd}"))?;
-
-    // Creating OwnedFd for stdio FDs is not safe.
-    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
-        bail!("File descriptor {raw_fd} is standard I/O descriptor");
-    }
-
-    // Reject RawFds that already have a corresponding OwnedFd.
-    if owned_fds.contains(&raw_fd) {
-        bail!("File descriptor {raw_fd} already owned");
-    }
-    owned_fds.push(raw_fd);
-
-    // SAFETY: Initializing OwnedFd for a RawFd provided in cmdline arguments.
-    // We checked that the integer value corresponds to a valid FD and that this
-    // is the first argument to claim its ownership.
-    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
-}
-
 fn check_vm_support() -> Result<()> {
     if hypervisor_props::is_any_vm_supported()? {
         Ok(())
@@ -109,6 +83,10 @@
 }
 
 fn main() {
+    // SAFETY: nobody has taken ownership of the inherited FDs yet.
+    unsafe { rustutils::inherited_fd::init_once() }
+        .expect("Failed to take ownership of inherited FDs");
+
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag(LOG_TAG)
@@ -120,11 +98,9 @@
 
     let args = Args::parse();
 
-    let mut owned_fds = vec![];
-    let rpc_server_fd = take_fd_ownership(args.rpc_server_fd, &mut owned_fds)
-        .expect("Failed to take ownership of rpc_server_fd");
-    let ready_fd = take_fd_ownership(args.ready_fd, &mut owned_fds)
-        .expect("Failed to take ownership of ready_fd");
+    let rpc_server_fd =
+        take_fd_ownership(args.rpc_server_fd).expect("Failed to take ownership of rpc_server_fd");
+    let ready_fd = take_fd_ownership(args.ready_fd).expect("Failed to take ownership of ready_fd");
 
     // Start thread pool for kernel Binder connection to VirtualizationServiceInternal.
     ProcessState::start_thread_pool();
diff --git a/android/virtualizationservice/aidl/Android.bp b/android/virtualizationservice/aidl/Android.bp
index c1bff5e..79a9d40 100644
--- a/android/virtualizationservice/aidl/Android.bp
+++ b/android/virtualizationservice/aidl/Android.bp
@@ -29,6 +29,7 @@
         rust: {
             enabled: true,
             apex_available: [
+                "//apex_available:platform",
                 "com.android.virt",
                 "com.android.compos",
                 "com.android.microfuchsia",
@@ -149,6 +150,7 @@
         rust: {
             enabled: true,
             apex_available: [
+                "//apex_available:platform",
                 "com.android.virt",
                 "com.android.compos",
                 "com.android.microfuchsia",
diff --git a/build/apex/Android.bp b/build/apex/Android.bp
index f493202..4916df7 100644
--- a/build/apex/Android.bp
+++ b/build/apex/Android.bp
@@ -107,6 +107,7 @@
             filesystems: microdroid_filesystem_images,
             prebuilts: [
                 "rialto_bin",
+                "android_bootloader_crosvm_aarch64",
             ],
         },
         x86_64: {
@@ -125,6 +126,9 @@
                 default: [],
             }),
             filesystems: microdroid_filesystem_images,
+            prebuilts: [
+                "android_bootloader_crosvm_x86_64",
+            ],
         },
     },
     binaries: [
@@ -132,13 +136,11 @@
         "vm",
     ],
     prebuilts: [
-        "features_com.android.virt.xml",
         "microdroid_initrd_debuggable",
         "microdroid_initrd_normal",
         "microdroid.json",
         "microdroid_kernel",
         "com.android.virt.init.rc",
-        "android_bootloader_crosvm_aarch64",
     ] + select(soong_config_variable("ANDROID", "avf_microdroid_guest_gki_version"), {
         "android15_66": [
             "microdroid_gki-android15-6.6_initrd_debuggable",
diff --git a/build/apex/permissions/Android.bp b/build/apex/permissions/Android.bp
index 0c925ce..678a4f2 100644
--- a/build/apex/permissions/Android.bp
+++ b/build/apex/permissions/Android.bp
@@ -21,4 +21,5 @@
     name: "features_com.android.virt.xml",
     sub_dir: "permissions",
     src: "features_com.android.virt.xml",
+    soc_specific: true,
 }
diff --git a/build/apex/product_packages.mk b/build/apex/product_packages.mk
index a024192..b2a4ca2 100644
--- a/build/apex/product_packages.mk
+++ b/build/apex/product_packages.mk
@@ -24,6 +24,7 @@
 
 PRODUCT_PACKAGES += \
     com.android.compos \
+    features_com.android.virt.xml
 
 # TODO(b/207336449): Figure out how to get these off /system
 PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST := \
diff --git a/build/debian/build.sh b/build/debian/build.sh
new file mode 100755
index 0000000..3d3820a
--- /dev/null
+++ b/build/debian/build.sh
@@ -0,0 +1,116 @@
+#!/bin/bash
+
+# This is a script to build a Debian image that can run in a VM created via AVF.
+# TODOs:
+# - Support x86_64 architecture
+# - Add Android-specific packages via a new class
+# - Use a stable release from debian-cloud-images
+
+show_help() {
+	echo Usage: $0 [OPTION]... [FILE]
+	echo Builds a debian image and save it to FILE.
+	echo Options:
+	echo -h         Pring usage and this help message and exit.
+}
+
+check_sudo() {
+	if [ "$EUID" -ne 0 ]; then
+		echo "Please run as root."
+		exit
+	fi
+}
+
+parse_options() {
+	while getopts ":h" option; do
+		case ${option} in
+			h)
+				show_help
+				exit;;
+		esac
+	done
+	if [ -n "$1" ]; then
+		built_image=$1
+	fi
+}
+
+install_prerequisites() {
+	apt update
+	DEBIAN_FRONTEND=noninteractive \
+	apt install --no-install-recommends --assume-yes \
+		ca-certificates \
+		debsums \
+		dosfstools \
+		fai-server \
+		fai-setup-storage \
+		fdisk \
+		make \
+		python3 \
+		python3-libcloud \
+		python3-marshmallow \
+		python3-pytest \
+		python3-yaml \
+		qemu-utils \
+		udev \
+		qemu-system-arm \
+		qemu-user-static
+
+        sed -i s/losetup\ -f/losetup\ -P\ -f/g /usr/sbin/fai-diskimage
+        sed -i 's/wget \$/wget -t 0 \$/g' /usr/share/debootstrap/functions
+
+        apt install --no-install-recommends --assume-yes curl
+        # just for testing
+        echo libseccomp: $(curl -is https://deb.debian.org/debian/pool/main/libs/libseccomp/libseccomp2_2.5.4-1+deb12u1_arm64.deb | head -n 1)
+        echo libsemanage-common: $(curl -is https://deb.debian.org/debian/pool/main/libs/libsemanage/libsemanage-common_3.4-1_all.deb | head -n 1)
+        echo libpcre2: $(curl -is https://deb.debian.org/debian/pool/main/p/pcre2/libpcre2-8-0_10.42-1_arm64.deb | head -n 1)
+}
+
+download_debian_cloud_image() {
+	local ver=master
+	local prj=debian-cloud-images
+	local url=https://salsa.debian.org/cloud-team/${prj}/-/archive/${ver}/${prj}-${ver}.tar.gz
+	local outdir=${debian_cloud_image}
+
+	mkdir -p ${outdir}
+	wget -O - ${url} | tar xz -C ${outdir} --strip-components=1
+}
+
+copy_android_config() {
+	local src=$(dirname $0)/fai_config
+	local dst=${config_space}
+
+	cp -R ${src}/* ${dst}
+	cp $(dirname $0)/image.yaml ${resources_dir}
+
+	local ttyd_version=1.7.7
+	local url=https://github.com/tsl0922/ttyd/releases/download/${ttyd_version}/ttyd.aarch64
+	mkdir -p ${dst}/files/usr/local/bin/ttyd
+	wget ${url} -O ${dst}/files/usr/local/bin/ttyd/AVF
+	chmod 777 ${dst}/files/usr/local/bin/ttyd/AVF
+}
+
+run_fai() {
+	local out=${built_image}
+	make -C ${debian_cloud_image} image_bookworm_nocloud_arm64
+	mv ${debian_cloud_image}/image_bookworm_nocloud_arm64.raw ${out}
+}
+
+clean_up() {
+	rm -rf ${workdir}
+}
+
+set -e
+trap clean_up EXIT
+
+built_image=image.raw
+workdir=$(mktemp -d)
+debian_cloud_image=${workdir}/debian_cloud_image
+debian_version=bookworm
+config_space=${debian_cloud_image}/config_space/${debian_version}
+resources_dir=${debian_cloud_image}/src/debian_cloud_images/resources
+check_sudo
+parse_options $@
+install_prerequisites
+download_debian_cloud_image
+copy_android_config
+run_fai
+fdisk -l image.raw
diff --git a/build/debian/fai_config/class/AVF.var b/build/debian/fai_config/class/AVF.var
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/build/debian/fai_config/class/AVF.var
diff --git a/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF b/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF
new file mode 100644
index 0000000..f71557d
--- /dev/null
+++ b/build/debian/fai_config/files/etc/systemd/system/ttyd.service/AVF
@@ -0,0 +1,12 @@
+[Unit]
+Description=TTYD
+After=syslog.target
+After=network.target
+[Service]
+ExecStart=/usr/local/bin/ttyd -W screen -aAxR -S main login
+Type=simple
+Restart=always
+User=root
+Group=root
+[Install]
+WantedBy=multi-user.target
diff --git a/build/debian/fai_config/files/etc/systemd/system/vsockip.service/AVF b/build/debian/fai_config/files/etc/systemd/system/vsockip.service/AVF
new file mode 100644
index 0000000..a29020b
--- /dev/null
+++ b/build/debian/fai_config/files/etc/systemd/system/vsockip.service/AVF
@@ -0,0 +1,12 @@
+[Unit]
+Description=vsock ip service
+After=syslog.target
+After=network.target
+[Service]
+ExecStart=/usr/bin/python3 /usr/local/bin/vsock.py
+Type=simple
+Restart=always
+User=root
+Group=root
+[Install]
+WantedBy=multi-user.target
diff --git a/build/debian/fai_config/files/usr/local/bin/vsock.py/AVF b/build/debian/fai_config/files/usr/local/bin/vsock.py/AVF
new file mode 100755
index 0000000..292d953
--- /dev/null
+++ b/build/debian/fai_config/files/usr/local/bin/vsock.py/AVF
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+
+import socket
+
+# Constants for vsock (from linux/vm_sockets.h)
+AF_VSOCK = 40
+SOCK_STREAM = 1
+VMADDR_CID_ANY = -1
+
+def get_local_ip():
+    """Retrieves the first IPv4 address found on the system.
+
+    Returns:
+        str: The local IPv4 address, or '127.0.0.1' if no IPv4 address is found.
+    """
+
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    try:
+        s.connect(('8.8.8.8', 80))
+        ip = s.getsockname()[0]
+    except Exception:
+        ip = '127.0.0.1'
+    finally:
+        s.close()
+    return ip
+
+def main():
+    PORT = 1024
+
+    # Create a vsock socket
+    server_socket = socket.socket(AF_VSOCK, SOCK_STREAM)
+
+    # Bind the socket to the server address
+    server_address = (VMADDR_CID_ANY, PORT)
+    server_socket.bind(server_address)
+
+    # Listen for incoming connections
+    server_socket.listen(1)
+    print(f"VSOCK server listening on port {PORT}...")
+
+    while True:
+        # Accept a connection
+        connection, client_address = server_socket.accept()
+        print(f"Connection from: {client_address}")
+
+        try:
+            # Get the local IP address
+            local_ip = get_local_ip()
+
+            # Send the IP address to the client
+            connection.sendall(local_ip.encode())
+        finally:
+            # Close the connection
+            connection.close()
+
+if __name__ == "__main__":
+    main()
diff --git a/build/debian/fai_config/hooks/extrbase.BASE b/build/debian/fai_config/hooks/extrbase.BASE
new file mode 100755
index 0000000..05d1e96
--- /dev/null
+++ b/build/debian/fai_config/hooks/extrbase.BASE
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -euE
+
+touch "${LOGDIR}/skip.extrbase"
+
+debootstrap --verbose --variant minbase --arch "$DEBOOTSTRAP_ARCH" "$SUITE" "$FAI_ROOT" "$DEBOOTSTRAP_MIRROR"
diff --git a/build/debian/fai_config/hooks/partition.ARM64 b/build/debian/fai_config/hooks/partition.ARM64
new file mode 100755
index 0000000..b3b603b
--- /dev/null
+++ b/build/debian/fai_config/hooks/partition.ARM64
@@ -0,0 +1,53 @@
+#!/bin/sh
+set -eu
+touch $LOGDIR/skip.partition
+
+set -- $disklist
+device=/dev/$1
+
+wait_for_device() {
+  for s in $(seq 10); do
+    if [ -e "$1" ]; then
+      break
+    fi
+    sleep 1
+  done
+}
+
+sfdisk "$device" << EOF
+label: gpt
+unit: sectors
+
+# EFI system
+p15 : start=2048, size=260096, type="EFI System", uuid=${PARTUUID_ESP}
+# Linux
+p1 : start=262144, type="Linux root (ARM-64)", uuid=${PARTUUID_ROOT}
+EOF
+
+file=$(losetup -O BACK-FILE ${device} | tail -1)
+
+root_offset=$(parted -m ${device} unit B print | awk -F '[B:]' '/1:/{ print $2 }')
+root_size=$(  parted -m ${device} unit B print | awk -F '[B:]' '/1:/{ print $6 }')
+efi_offset=$( parted -m ${device} unit B print | awk -F '[B:]' '/15:/{ print $2 }')
+efi_size=$(   parted -m ${device} unit B print | awk -F '[B:]' '/15:/{ print $6 }')
+device_root=$(losetup -o ${root_offset} --sizelimit ${root_size} --show -f ${file})
+device_efi=$(losetup -o ${efi_offset} --sizelimit ${efi_size} --show -f ${file})
+rm -f ${device}p1
+rm -f ${device}p15
+ln -sf ${device_root} ${device}p1
+ln -sf ${device_efi} ${device}p15
+
+ls -al /dev/loop*
+losetup -a -l
+parted ${device} unit B print
+
+partprobe "$device"
+
+wait_for_device "$device_root"
+mkfs.ext4 -U "$FSUUID_ROOT" "$device_root"
+tune2fs -c 0 -i 0 "$device_root"
+
+wait_for_device "$device_efi"
+mkfs.vfat "$device_efi"
+
+parted ${device} unit B print
diff --git a/build/debian/fai_config/package_config/AVF b/build/debian/fai_config/package_config/AVF
new file mode 100644
index 0000000..7d86d41
--- /dev/null
+++ b/build/debian/fai_config/package_config/AVF
@@ -0,0 +1,4 @@
+PACKAGES install
+
+# Just for testing
+tree
diff --git a/build/debian/fai_config/scripts/AVF/10-systemd b/build/debian/fai_config/scripts/AVF/10-systemd
new file mode 100755
index 0000000..e04a562
--- /dev/null
+++ b/build/debian/fai_config/scripts/AVF/10-systemd
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+chmod +x $target/usr/local/bin/ttyd
+chmod +x $target/usr/local/bin/vsock.py
+ln -s /etc/systemd/system/ttyd.service $target/etc/systemd/system/multi-user.target.wants/ttyd.service
+ln -s /etc/systemd/system/vsockip.service $target/etc/systemd/system/multi-user.target.wants/vsockip.service
\ No newline at end of file
diff --git a/build/debian/image.yaml b/build/debian/image.yaml
new file mode 100644
index 0000000..eb42a07
--- /dev/null
+++ b/build/debian/image.yaml
@@ -0,0 +1,60 @@
+# After modifications, please call:
+# "python3 -m debian_cloud_images.cli.generate_ci .gitlab/ci/generated.yml"
+---
+apiVersion: cloud.debian.org/v1alpha1
+kind: ImageConfig
+
+archs:
+- name: amd64
+  azureName: X64
+  ociArch: amd64
+  faiClasses: [AMD64]
+- name: arm64
+  azureName: Arm64
+  ociArch: arm64
+  faiClasses: [ARM64]
+- name: ppc64el
+  faiClasses: [PPC64EL]
+  ociArch: ppc64le
+- name: riscv64
+  faiClasses: [RISCV64]
+  ociArch: riscv64
+
+releases:
+- name: bookworm
+  basename: bookworm
+  id: '12'
+  baseid: '12'
+  faiClasses: [BOOKWORM, LINUX_VERSION_BASE, EXTRAS]
+  matches:
+  - matchArches: [amd64, arm64, ppc64el]
+- name: bookworm-backports
+  basename: bookworm-backports
+  id: 12-backports
+  baseid: '12'
+  faiClasses: [BOOKWORM, LINUX_VERSION_BACKPORTS, EXTRAS]
+  matches:
+  - matchArches: [amd64, arm64, ppc64el]
+- name: trixie
+  basename: trixie
+  id: '13'
+  baseid: '13'
+  faiClasses: [TRIXIE, LINUX_VERSION_BASE, EXTRAS]
+  matches:
+  - matchArches: [amd64, arm64, ppc64el]
+
+vendors:
+- name: nocloud
+  faiClasses: [SYSTEM_BOOT, NOCLOUD, LINUX_VARIANT_BASE, TIME_SYSTEMD, AVF]
+  size: 2
+
+types:
+- name: dev
+  faiClasses: [TYPE_DEV]
+  outputName: 'debian-{release}-{vendor}-{arch}-{build_type}-{build_id}-{version}'
+  outputVersion: '{version}'
+  outputVersionAzure: '0.0.{version!s}'
+- name: official
+  outputName: 'debian-{release}-{vendor}-{arch}-{build_type}-{version}'
+  outputVersion: '{date}-{version}'
+  outputVersionAzure: '0.{date!s}.{version!s}'
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/build.sh b/build/debian/kokoro/gcp_ubuntu_docker/build.sh
new file mode 100644
index 0000000..fb2a1a3
--- /dev/null
+++ b/build/debian/kokoro/gcp_ubuntu_docker/build.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+set -e
+
+cd "${KOKORO_ARTIFACTS_DIR}/git/avf/build/debian/"
+sudo losetup -D
+sudo ./build.sh
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg b/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
new file mode 100644
index 0000000..d92031e
--- /dev/null
+++ b/build/debian/kokoro/gcp_ubuntu_docker/continuous.cfg
@@ -0,0 +1,7 @@
+# -*- protobuffer -*-
+# proto-file: google3/devtools/kokoro/config/proto/build.proto
+# proto-message: BuildConfig
+
+# Location of the bash script. Should have value <git_on_borg_scm.name>/<path_from_repository_root>.
+# git_on_borg_scm.name is specified in the job configuration (next section).
+build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/build.sh"
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/presubmit.cfg b/build/debian/kokoro/gcp_ubuntu_docker/presubmit.cfg
new file mode 100644
index 0000000..d92031e
--- /dev/null
+++ b/build/debian/kokoro/gcp_ubuntu_docker/presubmit.cfg
@@ -0,0 +1,7 @@
+# -*- protobuffer -*-
+# proto-file: google3/devtools/kokoro/config/proto/build.proto
+# proto-message: BuildConfig
+
+# Location of the bash script. Should have value <git_on_borg_scm.name>/<path_from_repository_root>.
+# git_on_borg_scm.name is specified in the job configuration (next section).
+build_file: "avf/build/debian/kokoro/gcp_ubuntu_docker/build.sh"
diff --git a/docs/device_assignment.md b/docs/device_assignment.md
index 4b2296c..6011d8f 100644
--- a/docs/device_assignment.md
+++ b/docs/device_assignment.md
@@ -205,6 +205,18 @@
 * `<sysfs_path>`: Sysfs path of the device in host, used to bind to the VFIO
   driver. Must be non-empty and unique in the XML.
 
+### List support assignable devices
+
+In order to query list of the devices that can be assigned to a pVM, run the
+following command:
+
+```bash
+adb shell /apex/com.android.virt/bin/vm info
+```
+
+All supported assignable devices will be located under the "Assignable devices:"
+section of the output.
+
 ## Boot with VM DTBO
 
 Bootloader should provide VM DTBO to both Android and pvmfw.
diff --git a/docs/pvm_dice_chain.md b/docs/pvm_dice_chain.md
index 11cdb6f..67d1f28 100644
--- a/docs/pvm_dice_chain.md
+++ b/docs/pvm_dice_chain.md
@@ -6,11 +6,13 @@
 
 ![][pvm-dice-chain-img]
 
-The full RKP VM DICE chain, starting from `UDS_Pub` rooted in ROM, is
-sent to the RKP server during [pVM remote attestation][vm-attestation].
+The full [RKP VM DICE chain][rkpvm-dice-chain], starting from `UDS_Pub`
+rooted in ROM, is sent to the RKP server during
+[pVM remote attestation][vm-attestation].
 
 [vm-attestation]: vm_remote_attestation.md
 [pvm-dice-chain-img]: img/pvm-dice.png
+[rkpvm-dice-chain]: vm_remote_attestation.md#rkp-vm-marker
 
 ## Key derivation
 
diff --git a/guest/authfs_service/src/main.rs b/guest/authfs_service/src/main.rs
index 97e684d..be0f1b2 100644
--- a/guest/authfs_service/src/main.rs
+++ b/guest/authfs_service/src/main.rs
@@ -28,7 +28,6 @@
 use rustutils::sockets::android_get_control_socket;
 use std::ffi::OsString;
 use std::fs::{create_dir, read_dir, remove_dir_all, remove_file};
-use std::os::unix::io::{FromRawFd, OwnedFd};
 use std::sync::atomic::{AtomicUsize, Ordering};
 
 use authfs_aidl_interface::aidl::com::android::virt::fs::AuthFsConfig::AuthFsConfig;
@@ -108,27 +107,11 @@
     Ok(())
 }
 
-/// Prepares a socket file descriptor for the authfs service.
-///
-/// # Safety requirement
-///
-/// The caller must ensure that this function is the only place that claims ownership
-/// of the file descriptor and it is called only once.
-unsafe fn prepare_authfs_service_socket() -> Result<OwnedFd> {
-    let raw_fd = android_get_control_socket(AUTHFS_SERVICE_SOCKET_NAME)?;
-
-    // Creating OwnedFd for stdio FDs is not safe.
-    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
-        bail!("File descriptor {raw_fd} is standard I/O descriptor");
-    }
-    // SAFETY: Initializing OwnedFd for a RawFd created by the init.
-    // We checked that the integer value corresponds to a valid FD and that the caller
-    // ensures that this is the only place to claim its ownership.
-    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
-}
-
 #[allow(clippy::eq_op)]
 fn try_main() -> Result<()> {
+    // SAFETY: nobody has taken ownership of the inherited FDs yet.
+    unsafe { rustutils::inherited_fd::init_once()? };
+
     let debuggable = env!("TARGET_BUILD_VARIANT") != "user";
     let log_level = if debuggable { log::LevelFilter::Trace } else { log::LevelFilter::Info };
     android_logger::init_once(
@@ -137,8 +120,7 @@
 
     clean_up_working_directory()?;
 
-    // SAFETY: This is the only place we take the ownership of the fd of the authfs service.
-    let socket_fd = unsafe { prepare_authfs_service_socket()? };
+    let socket_fd = android_get_control_socket(AUTHFS_SERVICE_SOCKET_NAME)?;
     let service = AuthFsService::new_binder(debuggable).as_binder();
     debug!("{} is starting as a rpc service.", AUTHFS_SERVICE_SOCKET_NAME);
     let server = RpcServer::new_bound_socket(service, socket_fd)?;
diff --git a/guest/microdroid_manager/src/main.rs b/guest/microdroid_manager/src/main.rs
index 8186e9d..fa089fa 100644
--- a/guest/microdroid_manager/src/main.rs
+++ b/guest/microdroid_manager/src/main.rs
@@ -56,7 +56,7 @@
 use std::ffi::CString;
 use std::fs::{self, create_dir, File, OpenOptions};
 use std::io::{Read, Write};
-use std::os::unix::io::{FromRawFd, OwnedFd};
+use std::os::unix::io::OwnedFd;
 use std::os::unix::process::CommandExt;
 use std::os::unix::process::ExitStatusExt;
 use std::path::Path;
@@ -170,6 +170,9 @@
 }
 
 fn main() -> Result<()> {
+    // SAFETY: nobody has taken ownership of the inherited FDs yet.
+    unsafe { rustutils::inherited_fd::init_once()? };
+
     // If debuggable, print full backtrace to console log with stdio_to_kmsg
     if is_debuggable()? {
         env::set_var("RUST_BACKTRACE", "full");
@@ -199,13 +202,7 @@
     );
     info!("started.");
 
-    // SAFETY: This is the only place we take the ownership of the fd of the vm payload service.
-    //
-    // To ensure that the CLOEXEC flag is set on the file descriptor as early as possible,
-    // it is necessary to fetch the socket corresponding to vm_payload_service at the
-    // very beginning, as android_get_control_socket() sets the CLOEXEC flag on the file
-    // descriptor.
-    let vm_payload_service_fd = unsafe { prepare_vm_payload_service_socket()? };
+    let vm_payload_service_fd = android_get_control_socket(VM_PAYLOAD_SERVICE_SOCKET_NAME)?;
 
     load_crashkernel_if_supported().context("Failed to load crashkernel")?;
 
@@ -486,25 +483,6 @@
         .context("Could not connect to IVirtualMachineService")
 }
 
-/// Prepares a socket file descriptor for the vm payload service.
-///
-/// # Safety
-///
-/// The caller must ensure that this function is the only place that claims ownership
-/// of the file descriptor and it is called only once.
-unsafe fn prepare_vm_payload_service_socket() -> Result<OwnedFd> {
-    let raw_fd = android_get_control_socket(VM_PAYLOAD_SERVICE_SOCKET_NAME)?;
-
-    // Creating OwnedFd for stdio FDs is not safe.
-    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
-        bail!("File descriptor {raw_fd} is standard I/O descriptor");
-    }
-    // SAFETY: Initializing OwnedFd for a RawFd created by the init.
-    // We checked that the integer value corresponds to a valid FD and that the caller
-    // ensures that this is the only place to claim its ownership.
-    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
-}
-
 fn is_strict_boot() -> bool {
     Path::new(AVF_STRICT_BOOT).exists()
 }
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
index cb21ccf..de1b081 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -78,7 +78,8 @@
     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";
+    private static final String U_BOOT_PREBUILT_PATH_ARM = "/apex/com.android.virt/etc/u-boot.bin";
+    private static final String U_BOOT_PREBUILT_PATH_X86 = "/apex/com.android.virt/etc/u-boot.rom";
 
     // These define the schema of the config file persisted on disk.
     // Please bump up the version number when adding a new key.
@@ -668,7 +669,11 @@
                         .orElse(null);
 
         if (config.kernel == null && config.bootloader == null) {
-            config.bootloader = openOrNull(new File(U_BOOT_PREBUILT_PATH), MODE_READ_ONLY);
+          if (Arrays.stream(Build.SUPPORTED_ABIS).anyMatch("x86_64"::equals)) {
+            config.bootloader = openOrNull(new File(U_BOOT_PREBUILT_PATH_X86), MODE_READ_ONLY);
+          } else {
+            config.bootloader = openOrNull(new File(U_BOOT_PREBUILT_PATH_ARM), MODE_READ_ONLY);
+          }
         }
 
         config.params =
diff --git a/libs/libvmclient/Android.bp b/libs/libvmclient/Android.bp
index 5bd59da..d318d0e 100644
--- a/libs/libvmclient/Android.bp
+++ b/libs/libvmclient/Android.bp
@@ -23,6 +23,7 @@
         "com.android.compos",
         "com.android.microfuchsia",
         "com.android.virt",
+        "//apex_available:platform",
     ],
 }
 
diff --git a/libs/libvmclient/src/lib.rs b/libs/libvmclient/src/lib.rs
index bc9d683..ce7d5a5 100644
--- a/libs/libvmclient/src/lib.rs
+++ b/libs/libvmclient/src/lib.rs
@@ -55,6 +55,7 @@
     time::Duration,
 };
 
+const EARLY_VIRTMGR_PATH: &str = "/apex/com.android.virt/bin/early_virtmgr";
 const VIRTMGR_PATH: &str = "/apex/com.android.virt/bin/virtmgr";
 const VIRTMGR_THREADS: usize = 2;
 
@@ -122,10 +123,20 @@
     /// Spawns a new instance of virtmgr, a child process that will host
     /// the VirtualizationService AIDL service.
     pub fn new() -> Result<VirtualizationService, io::Error> {
+        Self::new_with_path(VIRTMGR_PATH)
+    }
+
+    /// Spawns a new instance of early_virtmgr, a child process that will host
+    /// the VirtualizationService AIDL service for early VMs.
+    pub fn new_early() -> Result<VirtualizationService, io::Error> {
+        Self::new_with_path(EARLY_VIRTMGR_PATH)
+    }
+
+    fn new_with_path(virtmgr_path: &str) -> Result<VirtualizationService, io::Error> {
         let (wait_fd, ready_fd) = posix_pipe()?;
         let (client_fd, server_fd) = posix_socketpair()?;
 
-        let mut command = Command::new(VIRTMGR_PATH);
+        let mut command = Command::new(virtmgr_path);
         // Can't use BorrowedFd as it doesn't implement Display
         command.arg("--rpc-server-fd").arg(format!("{}", server_fd.as_raw_fd()));
         command.arg("--ready-fd").arg(format!("{}", ready_fd.as_raw_fd()));
diff --git a/libs/vm_launcher_lib/Android.bp b/libs/vm_launcher_lib/Android.bp
index 8591c8d..cb6fc9e 100644
--- a/libs/vm_launcher_lib/Android.bp
+++ b/libs/vm_launcher_lib/Android.bp
@@ -9,5 +9,12 @@
         "//apex_available:platform",
         "com.android.virt",
     ],
-    sdk_version: "system_current",
+    platform_apis: true,
+    static_libs: [
+        "gson",
+    ],
+    libs: [
+        "framework-virtualization.impl",
+        "framework-annotations-lib",
+    ],
 }
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ConfigJson.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
similarity index 100%
rename from android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ConfigJson.java
rename to libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Logger.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Logger.java
similarity index 100%
rename from android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Logger.java
rename to libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Logger.java
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Runner.java
similarity index 98%
rename from android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java
rename to libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Runner.java
index a5f58fe..9b97fee 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/Runner.java
@@ -30,7 +30,7 @@
 
 /** Utility class for creating a VM and waiting for it to finish. */
 class Runner {
-    private static final String TAG = MainActivity.TAG;
+    private static final String TAG = Runner.class.getSimpleName();
     private final VirtualMachine mVirtualMachine;
     private final Callback mCallback;
 
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
similarity index 100%
rename from android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
rename to libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index 2eca2fa..d0838a6 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -16,7 +16,6 @@
     static_libs: [
         "MicrodroidHostTestHelper",
         "compatibility-host-util",
-        "cts-host-utils",
         "cts-statsd-atom-host-test-utils",
         "microdroid_payload_metadata",
     ],
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 0f7be20..2d55d66 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -32,12 +32,11 @@
 
 import static java.util.stream.Collectors.toList;
 
-import android.cts.host.utils.DeviceJUnit4ClassRunnerWithParameters;
-import android.cts.host.utils.DeviceJUnit4Parameterized;
 import android.cts.statsdatom.lib.ConfigUtils;
 import android.cts.statsdatom.lib.ReportUtils;
 
 import com.android.compatibility.common.util.CddTest;
+import com.android.compatibility.common.util.VsrTest;
 import com.android.microdroid.test.common.ProcessUtil;
 import com.android.microdroid.test.host.CommandRunner;
 import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
@@ -48,6 +47,7 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.TestDevice;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
+import com.android.tradefed.testtype.junit4.DeviceParameterizedRunner;
 import com.android.tradefed.util.CommandResult;
 import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.FileUtil;
@@ -55,6 +55,9 @@
 import com.android.tradefed.util.xml.AbstractXmlParser;
 import com.android.virt.PayloadMetadata;
 
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+
 import org.json.JSONArray;
 import org.json.JSONObject;
 import org.junit.After;
@@ -64,8 +67,6 @@
 import org.junit.Test;
 import org.junit.rules.TestName;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.UseParametersRunnerFactory;
 import org.xml.sax.Attributes;
 import org.xml.sax.helpers.DefaultHandler;
 
@@ -75,8 +76,8 @@
 import java.io.PipedOutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -87,8 +88,7 @@
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
-@RunWith(DeviceJUnit4Parameterized.class)
-@UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class)
+@RunWith(DeviceParameterizedRunner.class)
 public class MicrodroidHostTests extends MicrodroidHostTestCaseBase {
     private static final String APK_NAME = "MicrodroidTestApp.apk";
     private static final String APK_UPDATED_NAME = "MicrodroidTestAppUpdated.apk";
@@ -111,26 +111,38 @@
         }
     }
 
-    @Parameterized.Parameters(name = "protectedVm={0},gki={1}")
-    public static Collection<Object[]> params() {
-        List<Object[]> ret = new ArrayList<>();
-        ret.add(new Object[] {true /* protectedVm */, null /* use microdroid kernel */});
-        ret.add(new Object[] {false /* protectedVm */, null /* use microdroid kernel */});
+    // This map is needed because the parameterizer `DeviceParameterizedRunner` doesn't support "-"
+    // in test names. The key is the test name, while the value is the actual kernel version.
+    private static HashMap<String, String> sGkiVersions = new HashMap<>();
+
+    private static void initGkiVersions() {
+        if (!sGkiVersions.isEmpty()) {
+            return;
+        }
+        sGkiVersions.put("null", null); /* use microdroid kernel */
         // TODO(b/302465542): run only the latest GKI on presubmit to reduce running time
         for (String gki : SUPPORTED_GKI_VERSIONS) {
-            ret.add(new Object[] {true /* protectedVm */, gki});
-            ret.add(new Object[] {false /* protectedVm */, gki});
+            String key = gki.split("-")[0];
+            assertThat(sGkiVersions.containsKey(key)).isFalse();
+            sGkiVersions.put(key, gki);
+        }
+    }
+
+    public static List<Object[]> params() {
+        List<Object[]> ret = new ArrayList<>();
+        for (Object[] gki : gkiVersions()) {
+            ret.add(new Object[] {true /* protectedVm */, gki[0]});
+            ret.add(new Object[] {false /* protectedVm */, gki[0]});
         }
         return ret;
     }
 
-    @Parameterized.Parameter(0)
-    public boolean mProtectedVm;
-
-    @Parameterized.Parameter(1)
-    public String mGki;
-
-    private String mOs;
+    public static List<Object[]> gkiVersions() {
+        initGkiVersions();
+        return sGkiVersions.keySet().stream()
+                .map(gki -> new Object[] {gki})
+                .collect(Collectors.toList());
+    }
 
     @Rule public TestLogData mTestLogs = new TestLogData();
     @Rule public TestName mTestName = new TestName();
@@ -290,9 +302,11 @@
             File key,
             Map<String, File> keyOverrides,
             boolean isProtected,
-            boolean updateBootconfigs)
+            boolean updateBootconfigs,
+            String gki)
             throws Exception {
         CommandRunner android = new CommandRunner(getDevice());
+        gki = sGkiVersions.get(gki);
 
         File virtApexDir = FileUtil.createTempDir("virt_apex");
 
@@ -344,7 +358,8 @@
         //   - its idsig
 
         // Load etc/microdroid.json
-        File microdroidConfigFile = new File(virtApexEtcDir, mOs + ".json");
+        final String os = (gki == null) ? "microdroid" : "microdroid_gki-" + gki;
+        File microdroidConfigFile = new File(virtApexEtcDir, os + ".json");
         JSONObject config = new JSONObject(FileUtil.readStringFromFile(microdroidConfigFile));
 
         // Replace paths so that the config uses re-signed images from TEST_ROOT
@@ -360,7 +375,7 @@
         }
 
         // Add partitions to the second disk
-        final String initrdPath = TEST_ROOT + "etc/" + mOs + "_initrd_debuggable.img";
+        final String initrdPath = TEST_ROOT + "etc/" + os + "_initrd_debuggable.img";
         config.put("initrd", initrdPath);
         // Add instance image as a partition in disks[1]
         disks.put(
@@ -408,6 +423,9 @@
                         "--console " + CONSOLE_PATH,
                         "--log " + LOG_PATH,
                         configPath);
+        if (gki != null) {
+            args.add("--gki " + gki);
+        }
 
         PipedInputStream pis = new PipedInputStream();
         Process process = createRunUtil().runCmdInBackground(args, new PipedOutputStream(pis));
@@ -416,28 +434,34 @@
 
     @Test
     @CddTest
+    @VsrTest(requirements = {"VSR-7.1-001.008"})
     public void UpgradedPackageIsAcceptedWithSecretkeeper() throws Exception {
+        // Preconditions
+        assumeVmTypeSupported(true);
         assumeUpdatableVmSupported();
+
         getDevice().uninstallPackage(PACKAGE_NAME);
         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall= */ true);
-        ensureMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG);
+        ensureProtectedMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG);
 
         getDevice().uninstallPackage(PACKAGE_NAME);
         cleanUpVirtualizationTestSetup(getDevice());
         // Install the updated version of app (versionCode 6)
         getDevice().installPackage(findTestFile(APK_UPDATED_NAME), /* reinstall= */ true);
-        ensureMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG);
+        ensureProtectedMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG);
     }
 
     @Test
     @CddTest
+    @VsrTest(requirements = {"VSR-7.1-001.008"})
     public void DowngradedPackageIsRejectedProtectedVm() throws Exception {
-        assumeProtectedVm(); // Rollback protection is provided only for protected VM.
+        // Preconditions: Rollback protection is provided only for protected VM.
+        assumeVmTypeSupported(true);
 
         // Install the upgraded version (v6)
         getDevice().uninstallPackage(PACKAGE_NAME);
         getDevice().installPackage(findTestFile(APK_UPDATED_NAME), /* reinstall= */ true);
-        ensureMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG);
+        ensureProtectedMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG);
 
         getDevice().uninstallPackage(PACKAGE_NAME);
         cleanUpVirtualizationTestSetup(getDevice());
@@ -447,11 +471,11 @@
         assertThrows(
                 "pVM must fail to boot with downgraded payload apk",
                 DeviceRuntimeException.class,
-                () -> ensureMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG));
+                () -> ensureProtectedMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG));
     }
 
-    private void ensureMicrodroidBootsSuccessfully(String instanceIdPath, String instanceImgPath)
-            throws DeviceNotAvailableException {
+    private void ensureProtectedMicrodroidBootsSuccessfully(
+            String instanceIdPath, String instanceImgPath) throws DeviceNotAvailableException {
         final String configPath = "assets/vm_config.json";
         ITestDevice microdroid = null;
         int timeout = 30000; // 30 seconds
@@ -461,7 +485,7 @@
                             .debugLevel("full")
                             .memoryMib(minMemorySize())
                             .cpuTopology("match_host")
-                            .protectedVm(mProtectedVm)
+                            .protectedVm(true)
                             .instanceIdFile(instanceIdPath)
                             .instanceImgFile(instanceImgPath)
                             .setAdbConnectTimeoutMs(timeout)
@@ -476,10 +500,13 @@
     }
 
     @Test
+    @Parameters(method = "gkiVersions")
+    @TestCaseName("{method}_gki_{0}")
     @CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-6"})
-    public void protectedVmRunsPvmfw() throws Exception {
+    public void protectedVmRunsPvmfw(String gki) throws Exception {
         // Arrange
-        assumeProtectedVm();
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(true);
         final String configPath = "assets/vm_config_apex.json";
 
         // Act
@@ -489,7 +516,7 @@
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(true)
-                        .gki(mGki)
+                        .gki(sGkiVersions.get(gki))
                         .name("protected_vm_runs_pvmfw")
                         .build(getAndroidDevice());
 
@@ -506,10 +533,13 @@
     }
 
     @Test
-    @CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-6"})
-    public void protectedVmWithImageSignedWithDifferentKeyFailsToVerifyPayload() throws Exception {
-        // Arrange
-        assumeProtectedVm();
+    @Parameters(method = "gkiVersions")
+    @TestCaseName("{method}_gki_{0}")
+    @CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-5", "9.17/C-2-6"})
+    public void protectedVmWithImageSignedWithDifferentKeyFailsToVerifyPayload(String gki)
+            throws Exception {
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(true);
         File key = findTestFile("test.com.android.virt.pem");
 
         // Act
@@ -518,7 +548,8 @@
                         key,
                         /* keyOverrides= */ Map.of(),
                         /* isProtected= */ true,
-                        /* updateBootconfigs= */ true);
+                        /* updateBootconfigs= */ true,
+                        gki);
 
         // Assert
         vmInfo.mProcess.waitFor(5L, TimeUnit.SECONDS);
@@ -531,15 +562,23 @@
     }
 
     @Test
+    @Parameters(method = "gkiVersions")
+    @TestCaseName("{method}_gki_{0}")
     @CddTest(requirements = {"9.17/C-2-2", "9.17/C-2-6"})
-    public void testBootSucceedsWhenNonProtectedVmStartsWithImagesSignedWithDifferentKey()
+    public void testBootSucceedsWhenNonProtectedVmStartsWithImagesSignedWithDifferentKey(String gki)
             throws Exception {
-        assumeNonProtectedVm();
+        // Preconditions
+        assumeKernelSupported(gki);
+
         File key = findTestFile("test.com.android.virt.pem");
         Map<String, File> keyOverrides = Map.of();
         VmInfo vmInfo =
                 runMicrodroidWithResignedImages(
-                        key, keyOverrides, /* isProtected= */ false, /* updateBootconfigs= */ true);
+                        key,
+                        keyOverrides,
+                        /* isProtected= */ false,
+                        /* updateBootconfigs= */ true,
+                        gki);
         assertThatEventually(
                 100000,
                 () ->
@@ -551,16 +590,23 @@
     }
 
     @Test
-    @CddTest(requirements = {"9.17/C-2-2", "9.17/C-2-6"})
-    public void testBootFailsWhenVbMetaDigestDoesNotMatchBootconfig() throws Exception {
+    @Parameters(method = "gkiVersions")
+    @TestCaseName("{method}_gki_{0}")
+    @CddTest(requirements = {"9.17/C-2-2", "9.17/C-2-5", "9.17/C-2-6"})
+    public void testBootFailsWhenVbMetaDigestDoesNotMatchBootconfig(String gki) throws Exception {
         // protectedVmWithImageSignedWithDifferentKeyRunsPvmfw() is the protected case.
-        assumeNonProtectedVm();
+        assumeKernelSupported(gki);
+
         // Sign everything with key1 except vbmeta
         File key = findTestFile("test.com.android.virt.pem");
         // To be able to stop it, it should be a daemon.
         VmInfo vmInfo =
                 runMicrodroidWithResignedImages(
-                        key, Map.of(), /* isProtected= */ false, /* updateBootconfigs= */ false);
+                        key,
+                        Map.of(),
+                        /* isProtected= */ false,
+                        /* updateBootconfigs= */ false,
+                        gki);
         // Wait so that init can print errors to console (time in cuttlefish >> in real device)
         assertThatEventually(
                 100000,
@@ -608,7 +654,8 @@
     }
 
     private boolean isTombstoneGeneratedWithCmd(
-            boolean protectedVm, String configPath, String... crashCommand) throws Exception {
+            boolean protectedVm, String gki, String configPath, String... crashCommand)
+            throws Exception {
         CommandRunner android = new CommandRunner(getDevice());
         String testStartTime = android.runWithTimeout(1000, "date", "'+%Y-%m-%d %H:%M:%S.%N'");
 
@@ -618,7 +665,7 @@
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(protectedVm)
-                        .gki(mGki)
+                        .gki(sGkiVersions.get(gki))
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         mMicrodroidDevice.enableAdbRoot();
@@ -634,12 +681,19 @@
     }
 
     @Test
-    public void testTombstonesAreGeneratedUponUserspaceCrash() throws Exception {
+    @Parameters(method = "params")
+    @TestCaseName("{method}_protectedVm_{0}_gki_{1}")
+    public void testTombstonesAreGeneratedUponUserspaceCrash(boolean protectedVm, String gki)
+            throws Exception {
+        // Preconditions
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(protectedVm);
         // TODO(b/291867858): tombstones are failing in HWASAN enabled Microdroid.
         assumeFalse("tombstones are failing in HWASAN enabled Microdroid.", isHwasan());
         assertThat(
                         isTombstoneGeneratedWithCmd(
-                                mProtectedVm,
+                                protectedVm,
+                                gki,
                                 "assets/vm_config.json",
                                 "kill",
                                 "-SIGSEGV",
@@ -648,12 +702,19 @@
     }
 
     @Test
-    public void testTombstonesAreNotGeneratedIfNotExportedUponUserspaceCrash() throws Exception {
+    @Parameters(method = "params")
+    @TestCaseName("{method}_protectedVm_{0}_gki_{1}")
+    public void testTombstonesAreNotGeneratedIfNotExportedUponUserspaceCrash(
+            boolean protectedVm, String gki) throws Exception {
+        // Preconditions
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(protectedVm);
         // TODO(b/291867858): tombstones are failing in HWASAN enabled Microdroid.
         assumeFalse("tombstones are failing in HWASAN enabled Microdroid.", isHwasan());
         assertThat(
                         isTombstoneGeneratedWithCmd(
-                                mProtectedVm,
+                                protectedVm,
+                                gki,
                                 "assets/vm_config_no_tombstone.json",
                                 "kill",
                                 "-SIGSEGV",
@@ -662,13 +723,22 @@
     }
 
     @Test
+    @Parameters(method = "params")
+    @TestCaseName("{method}_protectedVm_{0}_gki_{1}")
     @Ignore("b/341087884") // TODO(b/341087884): fix & re-enable
-    public void testTombstonesAreGeneratedUponKernelCrash() throws Exception {
+    public void testTombstonesAreGeneratedUponKernelCrash(boolean protectedVm, String gki)
+            throws Exception {
+        // Preconditions
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(protectedVm);
         assumeFalse("Cuttlefish is not supported", isCuttlefish());
         assumeFalse("Skipping test because ramdump is disabled on user build", isUserBuild());
+
+        // Act
         assertThat(
                         isTombstoneGeneratedWithCmd(
-                                mProtectedVm,
+                                protectedVm,
+                                gki,
                                 "assets/vm_config.json",
                                 "echo",
                                 "c",
@@ -678,10 +748,12 @@
     }
 
     private boolean isTombstoneGeneratedWithVmRunApp(
-            boolean protectedVm, boolean debuggable, String... additionalArgs) throws Exception {
+            boolean protectedVm, String gki, boolean debuggable, String... additionalArgs)
+            throws Exception {
         // we can't use microdroid builder as it wants ADB connection (debuggable)
         CommandRunner android = new CommandRunner(getDevice());
         String testStartTime = android.runWithTimeout(1000, "date", "'+%Y-%m-%d %H:%M:%S.%N'");
+        gki = sGkiVersions.get(gki);
 
         android.run("rm", "-rf", TEST_ROOT + "*");
         android.run("mkdir", "-p", TEST_ROOT + "*");
@@ -708,9 +780,9 @@
         if (protectedVm) {
             cmd.add("--protected");
         }
-        if (mGki != null) {
+        if (gki != null) {
             cmd.add("--gki");
-            cmd.add(mGki);
+            cmd.add(gki);
         }
         Collections.addAll(cmd, additionalArgs);
 
@@ -718,52 +790,89 @@
         return isTombstoneReceivedFromHostLogcat(testStartTime);
     }
 
-    private boolean isTombstoneGeneratedWithCrashPayload(boolean protectedVm, boolean debuggable)
-            throws Exception {
+    private boolean isTombstoneGeneratedWithCrashPayload(
+            boolean protectedVm, String gki, boolean debuggable) throws Exception {
         return isTombstoneGeneratedWithVmRunApp(
-                protectedVm, debuggable, "--payload-binary-name", "MicrodroidCrashNativeLib.so");
+                protectedVm,
+                gki,
+                debuggable,
+                "--payload-binary-name",
+                "MicrodroidCrashNativeLib.so");
     }
 
     @Test
-    public void testTombstonesAreGeneratedWithCrashPayload() throws Exception {
+    @Parameters(method = "params")
+    @TestCaseName("{method}_protectedVm_{0}_gki_{1}")
+    public void testTombstonesAreGeneratedWithCrashPayload(boolean protectedVm, String gki)
+            throws Exception {
+        // Preconditions
         // TODO(b/291867858): tombstones are failing in HWASAN enabled Microdroid.
         assumeFalse("tombstones are failing in HWASAN enabled Microdroid.", isHwasan());
-        assertThat(isTombstoneGeneratedWithCrashPayload(mProtectedVm, /* debuggable= */ true))
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(protectedVm);
+
+        // Act
+        assertThat(isTombstoneGeneratedWithCrashPayload(protectedVm, gki, /* debuggable= */ true))
                 .isTrue();
     }
 
     @Test
-    public void testTombstonesAreNotGeneratedWithCrashPayloadWhenNonDebuggable() throws Exception {
+    @Parameters(method = "params")
+    @TestCaseName("{method}_protectedVm_{0}_gki_{1}")
+    public void testTombstonesAreNotGeneratedWithCrashPayloadWhenNonDebuggable(
+            boolean protectedVm, String gki) throws Exception {
+        // Preconditions
         // TODO(b/291867858): tombstones are failing in HWASAN enabled Microdroid.
         assumeFalse("tombstones are failing in HWASAN enabled Microdroid.", isHwasan());
-        assertThat(isTombstoneGeneratedWithCrashPayload(mProtectedVm, /* debuggable= */ false))
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(protectedVm);
+
+        // Act
+        assertThat(isTombstoneGeneratedWithCrashPayload(protectedVm, gki, /* debuggable= */ false))
                 .isFalse();
     }
 
-    private boolean isTombstoneGeneratedWithCrashConfig(boolean protectedVm, boolean debuggable)
-            throws Exception {
+    private boolean isTombstoneGeneratedWithCrashConfig(
+            boolean protectedVm, String gki, boolean debuggable) throws Exception {
         return isTombstoneGeneratedWithVmRunApp(
-                protectedVm, debuggable, "--config-path", "assets/vm_config_crash.json");
+                protectedVm, gki, debuggable, "--config-path", "assets/vm_config_crash.json");
     }
 
     @Test
-    public void testTombstonesAreGeneratedWithCrashConfig() throws Exception {
+    @Parameters(method = "params")
+    @TestCaseName("{method}_protectedVm_{0}_gki_{1}")
+    public void testTombstonesAreGeneratedWithCrashConfig(boolean protectedVm, String gki)
+            throws Exception {
+        // Preconditions
         // TODO(b/291867858): tombstones are failing in HWASAN enabled Microdroid.
         assumeFalse("tombstones are failing in HWASAN enabled Microdroid.", isHwasan());
-        assertThat(isTombstoneGeneratedWithCrashConfig(mProtectedVm, /* debuggable= */ true))
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(protectedVm);
+
+        // Act
+        assertThat(isTombstoneGeneratedWithCrashConfig(protectedVm, gki, /* debuggable= */ true))
                 .isTrue();
     }
 
     @Test
-    public void testTombstonesAreNotGeneratedWithCrashConfigWhenNonDebuggable() throws Exception {
+    @Parameters(method = "params")
+    @TestCaseName("{method}_protectedVm_{0}_gki_{1}")
+    public void testTombstonesAreNotGeneratedWithCrashConfigWhenNonDebuggable(
+            boolean protectedVm, String gki) throws Exception {
         // TODO(b/291867858): tombstones are failing in HWASAN enabled Microdroid.
         assumeFalse("tombstones are failing in HWASAN enabled Microdroid.", isHwasan());
-        assertThat(isTombstoneGeneratedWithCrashConfig(mProtectedVm, /* debuggable= */ false))
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(protectedVm);
+        assertThat(isTombstoneGeneratedWithCrashConfig(protectedVm, gki, /* debuggable= */ false))
                 .isFalse();
     }
 
     @Test
-    public void testTelemetryPushedAtoms() throws Exception {
+    @Parameters(method = "params")
+    @TestCaseName("{method}_protectedVm_{0}_gki_{1}")
+    public void testTelemetryPushedAtoms(boolean protectedVm, String gki) throws Exception {
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(protectedVm);
         // Reset statsd config and report before the test
         ConfigUtils.removeConfig(getDevice());
         ReportUtils.clearReports(getDevice());
@@ -784,8 +893,8 @@
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
-                        .protectedVm(mProtectedVm)
-                        .gki(mGki)
+                        .protectedVm(protectedVm)
+                        .gki(sGkiVersions.get(gki))
                         .name("test_telemetry_pushed_atoms")
                         .build(device);
         microdroid.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
@@ -815,7 +924,7 @@
             assertThat(atomVmCreationRequested.getHypervisor())
                     .isEqualTo(AtomsProto.VmCreationRequested.Hypervisor.PKVM);
         }
-        assertThat(atomVmCreationRequested.getIsProtected()).isEqualTo(mProtectedVm);
+        assertThat(atomVmCreationRequested.getIsProtected()).isEqualTo(protectedVm);
         assertThat(atomVmCreationRequested.getCreationSucceeded()).isTrue();
         assertThat(atomVmCreationRequested.getBinderExceptionCode()).isEqualTo(0);
         assertThat(atomVmCreationRequested.getVmIdentifier())
@@ -921,29 +1030,41 @@
     }
 
     @Test
+    @Parameters(method = "params")
+    @TestCaseName("{method}_protectedVm_{0}_gki_{1}")
     @CddTest(requirements = {"9.17/C-1-1", "9.17/C-1-2", "9.17/C/1-3"})
-    public void testMicrodroidBoots() throws Exception {
+    public void testMicrodroidBoots(boolean protectedVm, String gki) throws Exception {
+        // Preconditions
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(protectedVm);
+
         final String configPath = "assets/vm_config.json"; // path inside the APK
         testMicrodroidBootsWithBuilder(
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
-                        .protectedVm(mProtectedVm)
+                        .protectedVm(protectedVm)
                         .name("test_microdroid_boots")
-                        .gki(mGki));
+                        .gki(sGkiVersions.get(gki)));
     }
 
     @Test
-    public void testMicrodroidRamUsage() throws Exception {
+    @Parameters(method = "params")
+    @TestCaseName("{method}_protectedVm_{0}_gki_{1}")
+    public void testMicrodroidRamUsage(boolean protectedVm, String gki) throws Exception {
+        // Preconditions
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(protectedVm);
+
         final String configPath = "assets/vm_config.json";
         mMicrodroidDevice =
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
-                        .protectedVm(mProtectedVm)
-                        .gki(mGki)
+                        .protectedVm(protectedVm)
+                        .gki(sGkiVersions.get(gki))
                         .name("test_microdroid_ram_usage")
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
@@ -1125,8 +1246,12 @@
     }
 
     @Test
-    public void testDeviceAssignment() throws Exception {
-        // Check for preconditions
+    @Parameters(method = "params")
+    @TestCaseName("{method}_protectedVm_{0}_gki_{1}")
+    public void testDeviceAssignment(boolean protectedVm, String gki) throws Exception {
+        // Preconditions
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(protectedVm);
         assumeVfioPlatformSupported();
 
         List<AssignableDevice> devices = getAssignableDevices();
@@ -1136,7 +1261,7 @@
 
         // Try assign devices one by one
         for (AssignableDevice device : devices) {
-            launchWithDeviceAssignment(device.node);
+            launchWithDeviceAssignment(device.node, protectedVm, gki);
 
             String dtPath =
                     new CommandRunner(mMicrodroidDevice)
@@ -1158,7 +1283,8 @@
         }
     }
 
-    private void launchWithDeviceAssignment(String device) throws Exception {
+    private void launchWithDeviceAssignment(String device, boolean protectedVm, String gki)
+            throws Exception {
         Objects.requireNonNull(device);
         final String configPath = "assets/vm_config.json";
 
@@ -1167,8 +1293,8 @@
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
-                        .protectedVm(mProtectedVm)
-                        .gki(mGki)
+                        .protectedVm(protectedVm)
+                        .gki(sGkiVersions.get(gki))
                         .addAssignableDevice(device)
                         .build(getAndroidDevice());
 
@@ -1186,7 +1312,13 @@
     }
 
     @Test
-    public void testHugePages() throws Exception {
+    @Parameters(method = "params")
+    @TestCaseName("{method}_protectedVm_{0}_gki_{1}")
+    public void testHugePages(boolean protectedVm, String gki) throws Exception {
+        // Preconditions
+        assumeKernelSupported(gki);
+        assumeVmTypeSupported(protectedVm);
+
         ITestDevice device = getDevice();
         boolean disableRoot = !device.isAdbRoot();
         CommandRunner android = new CommandRunner(device);
@@ -1207,8 +1339,8 @@
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
-                        .protectedVm(mProtectedVm)
-                        .gki(mGki)
+                        .protectedVm(protectedVm)
+                        .gki(sGkiVersions.get(gki))
                         .hugePages(true)
                         .name("test_huge_pages")
                         .build(getAndroidDevice());
@@ -1230,19 +1362,6 @@
 
         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall= */ false);
 
-        // Skip test if given device doesn't support protected or non-protected VM.
-        assumeTrue(
-                "Microdroid is not supported for specific VM protection type",
-                getAndroidDevice().supportsMicrodroid(mProtectedVm));
-
-        if (mGki != null) {
-            assumeTrue(
-                    "GKI version \"" + mGki + "\" is not supported on this device",
-                    getSupportedGKIVersions().contains(mGki));
-        }
-
-        mOs = (mGki != null) ? "microdroid_gki-" + mGki : "microdroid";
-
         new CommandRunner(getDevice())
                 .tryRun(
                         "pm",
@@ -1265,14 +1384,6 @@
         getDevice().uninstallPackage(PACKAGE_NAME);
     }
 
-    private void assumeProtectedVm() {
-        assumeTrue("This test is only for protected VM.", mProtectedVm);
-    }
-
-    private void assumeNonProtectedVm() {
-        assumeFalse("This test is only for non-protected VM.", mProtectedVm);
-    }
-
     private void assumeVfioPlatformSupported() throws Exception {
         TestDevice device = getAndroidDevice();
         assumeTrue(
@@ -1302,4 +1413,19 @@
         runUtil.unsetEnvVariable("LD_LIBRARY_PATH");
         return runUtil;
     }
+
+    private void assumeKernelSupported(String gki) throws Exception {
+        String gkiVersion = sGkiVersions.get(gki);
+        if (gkiVersion != null) {
+            assumeTrue(
+                    "Skipping test as the GKI is not supported: " + gkiVersion,
+                    getSupportedGKIVersions().contains(gkiVersion));
+        }
+    }
+
+    private void assumeVmTypeSupported(boolean protectedVm) throws Exception {
+        assumeTrue(
+                "Microdroid is not supported for specific VM protection type",
+                getAndroidDevice().supportsMicrodroid(protectedVm));
+    }
 }
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 d38af45..c09f033 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -1212,7 +1212,7 @@
     }
 
     @Test
-    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7", "9.17/C-3-4"})
     public void instancesOfSameVmHaveDifferentCdis() throws Exception {
         assumeSupportedDevice();
         // TODO(b/325094712): VMs on CF with same payload have the same secret. This is because
@@ -1239,7 +1239,7 @@
     }
 
     @Test
-    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7", "9.17/C-3-4"})
     public void sameInstanceKeepsSameCdis() throws Exception {
         assumeSupportedDevice();
         assume().withMessage("Skip on CF. Too Slow. b/257270529").that(isCuttlefish()).isFalse();