Merge "avb: move Rust bindgen to libavb" into main
diff --git a/OWNERS b/OWNERS
index 310add7..e560cec 100644
--- a/OWNERS
+++ b/OWNERS
@@ -12,16 +12,19 @@
 # Other owners
 alanstokes@google.com
 aliceywang@google.com
-ardb@google.com
-ascull@google.com
 inseob@google.com
+jaewan@google.com
+jakobvukalovic@google.com
 jeffv@google.com
 jooyung@google.com
-mzyngier@google.com
+keirf@google.com
 ptosi@google.com
 qperret@google.com
 qwandor@google.com
-serbanc@google.com
+sebastianene@google.com
+seungjaeyoo@google.com
 shikhapanwar@google.com
+smostafa@google.com
 tabba@google.com
+vdonnefort@google.com
 victorhsieh@google.com
diff --git a/apex/Android.bp b/apex/Android.bp
index 765372a..ccbdb3b 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -103,7 +103,6 @@
         "microdroid_initrd_normal",
         "microdroid.json",
         "microdroid_kernel",
-        // rialto_bin is a prebuilt target wrapping the signed bare-metal service VM.
         "rialto_bin",
     ],
     host_required: [
diff --git a/apex/virtualizationservice.rc b/apex/virtualizationservice.rc
index be90904..8283594 100644
--- a/apex/virtualizationservice.rc
+++ b/apex/virtualizationservice.rc
@@ -22,7 +22,7 @@
 
 service vfio_handler /apex/com.android.virt/bin/vfio_handler
     user root
-    group root
+    group system
     interface aidl android.system.virtualizationservice_internal.IVfioHandler
     disabled
     oneshot
diff --git a/compos/common/binder.rs b/compos/common/binder.rs
index d3550f7..aea0072 100644
--- a/compos/common/binder.rs
+++ b/compos/common/binder.rs
@@ -16,7 +16,7 @@
 
 //! Helper for converting Error types to what Binder expects
 
-use binder::{Result as BinderResult, Status};
+use binder::{IntoBinderResult, Result as BinderResult};
 use log::warn;
 use std::fmt::Debug;
 
@@ -24,9 +24,9 @@
 /// preserving the content as far as possible.
 /// Also log the error if there is one.
 pub fn to_binder_result<T, E: Debug>(result: Result<T, E>) -> BinderResult<T> {
-    result.map_err(|e| {
+    result.or_service_specific_exception_with(-1, |e| {
         let message = format!("{:?}", e);
-        warn!("Returning binder error: {}", &message);
-        Status::new_service_specific_error_str(-1, Some(message))
+        warn!("Returning binder error: {message}");
+        message
     })
 }
diff --git a/compos/src/artifact_signer.rs b/compos/src/artifact_signer.rs
index 908e438..76da00a 100644
--- a/compos/src/artifact_signer.rs
+++ b/compos/src/artifact_signer.rs
@@ -63,7 +63,7 @@
     /// with accompanying sigature file.
     pub fn write_info_and_signature(self, info_path: &Path) -> Result<()> {
         let mut info = OdsignInfo::new();
-        info.file_hashes.extend(self.file_digests.into_iter());
+        info.file_hashes.extend(self.file_digests);
         let bytes = info.write_to_bytes()?;
 
         let signature = compos_key::sign(&bytes)?;
diff --git a/compos/src/compsvc.rs b/compos/src/compsvc.rs
index 8febd52..fe83ba2 100644
--- a/compos/src/compsvc.rs
+++ b/compos/src/compsvc.rs
@@ -33,7 +33,9 @@
 use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFsService::{
     IAuthFsService, AUTHFS_SERVICE_SOCKET_NAME,
 };
-use binder::{BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, Strong};
+use binder::{
+    BinderFeatures, ExceptionCode, Interface, IntoBinderResult, Result as BinderResult, Strong,
+};
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::{
     BnCompOsService, ICompOsService, OdrefreshArgs::OdrefreshArgs,
 };
@@ -66,29 +68,23 @@
     fn initializeSystemProperties(&self, names: &[String], values: &[String]) -> BinderResult<()> {
         let mut initialized = self.initialized.write().unwrap();
         if initialized.is_some() {
-            return Err(Status::new_exception_str(
-                ExceptionCode::ILLEGAL_STATE,
-                Some(format!("Already initialized: {:?}", initialized)),
-            ));
+            return Err(format!("Already initialized: {initialized:?}"))
+                .or_binder_exception(ExceptionCode::ILLEGAL_STATE);
         }
         *initialized = Some(false);
 
         if names.len() != values.len() {
-            return Err(Status::new_exception_str(
-                ExceptionCode::ILLEGAL_ARGUMENT,
-                Some(format!(
-                    "Received inconsistent number of keys ({}) and values ({})",
-                    names.len(),
-                    values.len()
-                )),
-            ));
+            return Err(format!(
+                "Received inconsistent number of keys ({}) and values ({})",
+                names.len(),
+                values.len()
+            ))
+            .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT);
         }
         for (name, value) in zip(names, values) {
             if !is_system_property_interesting(name) {
-                return Err(Status::new_exception_str(
-                    ExceptionCode::ILLEGAL_ARGUMENT,
-                    Some(format!("Received invalid system property {}", &name)),
-                ));
+                return Err(format!("Received invalid system property {name}"))
+                    .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT);
             }
             let result = system_properties::write(name, value);
             if result.is_err() {
@@ -103,10 +99,8 @@
     fn odrefresh(&self, args: &OdrefreshArgs) -> BinderResult<i8> {
         let initialized = *self.initialized.read().unwrap();
         if !initialized.unwrap_or(false) {
-            return Err(Status::new_exception_str(
-                ExceptionCode::ILLEGAL_STATE,
-                Some("Service has not been initialized"),
-            ));
+            return Err("Service has not been initialized")
+                .or_binder_exception(ExceptionCode::ILLEGAL_STATE);
         }
 
         to_binder_result(self.do_odrefresh(args))
diff --git a/compos/tests/java/android/compos/test/ComposTestCase.java b/compos/tests/java/android/compos/test/ComposTestCase.java
index 244d34e..4851321 100644
--- a/compos/tests/java/android/compos/test/ComposTestCase.java
+++ b/compos/tests/java/android/compos/test/ComposTestCase.java
@@ -192,7 +192,7 @@
                         .runTimedCmd(
                                 10000,
                                 validator.getAbsolutePath(),
-                                "verify-dice-chain",
+                                "dice-chain",
                                 bcc_file.getAbsolutePath());
         assertWithMessage("hwtrust failed").about(command_results()).that(result).isSuccess();
     }
diff --git a/docs/getting_started.md b/docs/getting_started.md
index d970c12..74f2012 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -99,7 +99,7 @@
 payload using the following command:
 
 ```shell
-package/modules/Virtualization/vm/vm_shell.sh start-microdroid --auto-connect -- --protected
+packages/modules/Virtualization/vm/vm_shell.sh start-microdroid --auto-connect -- --protected
 ```
 
 You will see the log messages like the below.
diff --git a/encryptedstore/Android.bp b/encryptedstore/Android.bp
index 94ebcfc..8ba5016 100644
--- a/encryptedstore/Android.bp
+++ b/encryptedstore/Android.bp
@@ -14,6 +14,7 @@
         "libclap",
         "libhex",
         "liblog_rust",
+        "libmicrodroid_uids",
         "libnix",
         "libdm_rust",
     ],
diff --git a/encryptedstore/src/main.rs b/encryptedstore/src/main.rs
index 1a16f49..2a698ea 100644
--- a/encryptedstore/src/main.rs
+++ b/encryptedstore/src/main.rs
@@ -21,24 +21,32 @@
 use anyhow::{ensure, Context, Result};
 use clap::arg;
 use dm::{crypt::CipherType, util};
-use log::info;
+use log::{error, info};
 use std::ffi::CString;
 use std::fs::{create_dir_all, OpenOptions};
 use std::io::{Error, Read, Write};
 use std::os::unix::ffi::OsStrExt;
-use std::os::unix::fs::FileTypeExt;
+use std::os::unix::fs::{FileTypeExt, PermissionsExt};
 use std::path::{Path, PathBuf};
 use std::process::Command;
 
 const MK2FS_BIN: &str = "/system/bin/mke2fs";
 const UNFORMATTED_STORAGE_MAGIC: &str = "UNFORMATTED-STORAGE";
 
-fn main() -> Result<()> {
+fn main() {
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag("encryptedstore")
             .with_min_level(log::Level::Info),
     );
+
+    if let Err(e) = try_main() {
+        error!("{:?}", e);
+        std::process::exit(1)
+    }
+}
+
+fn try_main() -> Result<()> {
     info!("Starting encryptedstore binary");
 
     let matches = clap_command().get_matches();
@@ -47,10 +55,12 @@
     let key = matches.get_one::<String>("key").unwrap();
     let mountpoint = Path::new(matches.get_one::<String>("mountpoint").unwrap());
     // Note this error context is used in MicrodroidTests.
-    encryptedstore_init(blkdevice, key, mountpoint).context(format!(
-        "Unable to initialize encryptedstore on {:?} & mount at {:?}",
-        blkdevice, mountpoint
-    ))?;
+    encryptedstore_init(blkdevice, key, mountpoint).with_context(|| {
+        format!(
+            "Unable to initialize encryptedstore on {:?} & mount at {:?}",
+            blkdevice, mountpoint
+        )
+    })?;
     Ok(())
 }
 
@@ -65,7 +75,7 @@
 fn encryptedstore_init(blkdevice: &Path, key: &str, mountpoint: &Path) -> Result<()> {
     ensure!(
         std::fs::metadata(blkdevice)
-            .context(format!("Failed to get metadata of {:?}", blkdevice))?
+            .with_context(|| format!("Failed to get metadata of {:?}", blkdevice))?
             .file_type()
             .is_block_device(),
         "The path:{:?} is not of a block device",
@@ -82,7 +92,12 @@
         info!("Freshly formatting the crypt device");
         format_ext4(&crypt_device)?;
     }
-    mount(&crypt_device, mountpoint).context(format!("Unable to mount {:?}", crypt_device))?;
+    mount(&crypt_device, mountpoint)
+        .with_context(|| format!("Unable to mount {:?}", crypt_device))?;
+    if needs_formatting {
+        std::fs::set_permissions(mountpoint, PermissionsExt::from_mode(0o770))
+            .context("Failed to chmod root directory")?;
+    }
     Ok(())
 }
 
@@ -124,6 +139,11 @@
 }
 
 fn format_ext4(device: &Path) -> Result<()> {
+    let root_dir_uid_gid = format!(
+        "root_owner={}:{}",
+        microdroid_uids::ROOT_UID,
+        microdroid_uids::MICRODROID_PAYLOAD_GID
+    );
     let mkfs_options = [
         "-j", // Create appropriate sized journal
         /* metadata_csum: enabled for filesystem integrity
@@ -131,20 +151,22 @@
          * 64bit: larger fields afforded by this feature enable full-strength checksumming.
          */
         "-O metadata_csum, extents, 64bit",
-        "-b 4096", // block size in the filesystem
+        "-b 4096", // block size in the filesystem,
+        "-E",
+        &root_dir_uid_gid,
     ];
     let mut cmd = Command::new(MK2FS_BIN);
     let status = cmd
         .args(mkfs_options)
         .arg(device)
         .status()
-        .context(format!("failed to execute {}", MK2FS_BIN))?;
+        .with_context(|| format!("failed to execute {}", MK2FS_BIN))?;
     ensure!(status.success(), "mkfs failed with {:?}", status);
     Ok(())
 }
 
 fn mount(source: &Path, mountpoint: &Path) -> Result<()> {
-    create_dir_all(mountpoint).context(format!("Failed to create {:?}", &mountpoint))?;
+    create_dir_all(mountpoint).with_context(|| format!("Failed to create {:?}", &mountpoint))?;
     let mount_options = CString::new(
         "fscontext=u:object_r:encryptedstore_fs:s0,context=u:object_r:encryptedstore_file:s0",
     )
diff --git a/libs/avflog/Android.bp b/libs/avflog/Android.bp
new file mode 100644
index 0000000..1ddfc7a
--- /dev/null
+++ b/libs/avflog/Android.bp
@@ -0,0 +1,30 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "libavflog.defaults",
+    crate_name: "avflog",
+    host_supported: true,
+    srcs: ["src/lib.rs"],
+    edition: "2021",
+    rustlibs: [
+        "liblog_rust",
+    ],
+}
+
+rust_library {
+    name: "libavflog",
+    defaults: ["libavflog.defaults"],
+    apex_available: [
+        "//apex_available:platform",
+        "//apex_available:anyapex",
+    ],
+}
+
+rust_test {
+    name: "libavflog.test",
+    defaults: ["libavflog.defaults"],
+    prefer_rlib: true,
+    test_suites: ["general-tests"],
+}
diff --git a/libs/avflog/TEST_MAPPING b/libs/avflog/TEST_MAPPING
new file mode 100644
index 0000000..921c4d8
--- /dev/null
+++ b/libs/avflog/TEST_MAPPING
@@ -0,0 +1,9 @@
+// When adding or removing tests here, don't forget to amend _all_modules list in
+// wireless/android/busytown/ath_config/configs/prod/avf/tests.gcl
+{
+  "avf-presubmit" : [
+    {
+      "name" : "libavflog.test"
+    }
+  ]
+}
diff --git a/libs/avflog/src/lib.rs b/libs/avflog/src/lib.rs
new file mode 100644
index 0000000..27c8628
--- /dev/null
+++ b/libs/avflog/src/lib.rs
@@ -0,0 +1,71 @@
+// Copyright 2023, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Provides random utilities for components in AVF
+
+use log::error;
+use std::fmt::Debug;
+
+/// Convenient trait for logging an error while returning it
+pub trait LogResult<T, E> {
+    /// If this is `Err`, the error is debug-formatted and is logged via `error!`.
+    fn with_log(self) -> Result<T, E>;
+}
+
+impl<T, E: Debug> LogResult<T, E> for Result<T, E> {
+    fn with_log(self) -> Result<T, E> {
+        self.map_err(|e| {
+            error!("{e:?}");
+            e
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use log::{LevelFilter, Log, Metadata, Record};
+    use std::cell::RefCell;
+    use std::io::{Error, ErrorKind};
+
+    struct TestLogger {
+        last_log: RefCell<String>,
+    }
+    static TEST_LOGGER: TestLogger = TestLogger { last_log: RefCell::new(String::new()) };
+
+    // SAFETY: TestLogger is used only inside the test which is single-treaded.
+    unsafe impl Sync for TestLogger {}
+
+    impl Log for TestLogger {
+        fn enabled(&self, _metadata: &Metadata) -> bool {
+            true
+        }
+        fn log(&self, record: &Record) {
+            *self.last_log.borrow_mut() = format!("{}", record.args());
+        }
+        fn flush(&self) {}
+    }
+
+    #[test]
+    fn test_logresult_emits_error_log() {
+        log::set_logger(&TEST_LOGGER).unwrap();
+        log::set_max_level(LevelFilter::Info);
+
+        let e = Error::from(ErrorKind::NotFound);
+        let res: Result<(), Error> = Err(e).with_log();
+
+        assert!(res.is_err());
+        assert_eq!(TEST_LOGGER.last_log.borrow().as_str(), "Kind(NotFound)");
+    }
+}
diff --git a/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index afc36d0..a305e03 100644
--- a/libs/libfdt/src/lib.rs
+++ b/libs/libfdt/src/lib.rs
@@ -16,8 +16,6 @@
 //! to a bare-metal environment.
 
 #![no_std]
-#![deny(unsafe_op_in_unsafe_fn)]
-#![deny(clippy::undocumented_unsafe_blocks)]
 
 mod iterators;
 
diff --git a/libs/microdroid_uids/Android.bp b/libs/microdroid_uids/Android.bp
new file mode 100644
index 0000000..497948d
--- /dev/null
+++ b/libs/microdroid_uids/Android.bp
@@ -0,0 +1,15 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_library {
+    name: "libmicrodroid_uids",
+    crate_name: "microdroid_uids",
+    srcs: ["src/lib.rs"],
+    edition: "2021",
+    // TODO(b/296393106): Figure out how/when to enable this
+    // cfgs: ["payload_not_root"],
+    apex_available: [
+        "com.android.virt",
+    ],
+}
diff --git a/libs/microdroid_uids/src/lib.rs b/libs/microdroid_uids/src/lib.rs
new file mode 100644
index 0000000..1f09c65
--- /dev/null
+++ b/libs/microdroid_uids/src/lib.rs
@@ -0,0 +1,24 @@
+// Copyright 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! User and group IDs within Microdroid
+
+/// Always the user ID of Root.
+pub const ROOT_UID: u32 = 0;
+
+/// Group ID shared by all payload users.
+pub const MICRODROID_PAYLOAD_GID: u32 = if cfg!(payload_not_root) { 6000 } else { 0 };
+
+/// User ID for the initial payload user.
+pub const MICRODROID_PAYLOAD_UID: u32 = if cfg!(payload_not_root) { 6000 } else { 0 };
diff --git a/libs/service_vm_comm/Android.bp b/libs/service_vm_comm/Android.bp
new file mode 100644
index 0000000..18397c5
--- /dev/null
+++ b/libs/service_vm_comm/Android.bp
@@ -0,0 +1,36 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "libservice_vm_comm_defaults",
+    crate_name: "service_vm_comm",
+    srcs: ["src/lib.rs"],
+    prefer_rlib: true,
+    apex_available: [
+        "com.android.virt",
+    ],
+}
+
+rust_library_rlib {
+    name: "libservice_vm_comm_nostd",
+    defaults: ["libservice_vm_comm_defaults"],
+    no_stdlibs: true,
+    stdlibs: [
+        "libcore.rust_sysroot",
+    ],
+    rustlibs: [
+        "libserde_nostd",
+    ],
+}
+
+rust_library {
+    name: "libservice_vm_comm",
+    defaults: ["libservice_vm_comm_defaults"],
+    rustlibs: [
+        "libserde",
+    ],
+    features: [
+        "std",
+    ],
+}
diff --git a/libs/service_vm_comm/src/lib.rs b/libs/service_vm_comm/src/lib.rs
new file mode 100644
index 0000000..c3d3ed5
--- /dev/null
+++ b/libs/service_vm_comm/src/lib.rs
@@ -0,0 +1,24 @@
+// Copyright 2023, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! This library contains the communication protocol used between the host
+//! and the service VM.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+extern crate alloc;
+
+mod message;
+
+pub use message::{Request, Response};
diff --git a/libs/service_vm_comm/src/message.rs b/libs/service_vm_comm/src/message.rs
new file mode 100644
index 0000000..ebbefcb
--- /dev/null
+++ b/libs/service_vm_comm/src/message.rs
@@ -0,0 +1,39 @@
+// Copyright 2023, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! This module contains the requests and responses definitions exchanged
+//! between the host and the service VM.
+
+use alloc::vec::Vec;
+
+use serde::{Deserialize, Serialize};
+
+/// Represents a request to be sent to the service VM.
+///
+/// Each request has a corresponding response item.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum Request {
+    /// Reverse the order of the bytes in the provided byte array.
+    /// Currently this is only used for testing.
+    Reverse(Vec<u8>),
+}
+
+/// Represents a response to a request sent to the service VM.
+///
+/// Each response corresponds to a specific request.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub enum Response {
+    /// Reverse the order of the bytes in the provided byte array.
+    Reverse(Vec<u8>),
+}
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 2d3f084..1e594b7 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -54,6 +54,8 @@
     deps: [
         "init_second_stage.microdroid",
         "microdroid_build_prop",
+        "microdroid_etc_passwd",
+        "microdroid_etc_group",
         "microdroid_init_debug_policy",
         "microdroid_init_rc",
         "microdroid_ueventd_rc",
@@ -156,6 +158,20 @@
     installable: false, // avoid collision with system partition's ueventd.rc
 }
 
+prebuilt_etc {
+    name: "microdroid_etc_passwd",
+    src: "microdroid_passwd",
+    filename: "passwd",
+    installable: false,
+}
+
+prebuilt_etc {
+    name: "microdroid_etc_group",
+    src: "microdroid_group",
+    filename: "group",
+    installable: false,
+}
+
 prebuilt_root {
     name: "microdroid_build_prop",
     filename: "build.prop",
diff --git a/microdroid/init.rc b/microdroid/init.rc
index 42033d6..c257cdb 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -12,6 +12,11 @@
 
 # Cgroups are mounted right before early-init using list from /etc/cgroups.json
 on early-init
+    # Android doesn't need kernel module autoloading, and it causes SELinux
+    # denials.  So disable it by setting modprobe to the empty string.  Note: to
+    # explicitly set a sysctl to an empty string, a trailing newline is needed.
+    write /proc/sys/kernel/modprobe \n
+
     # set RLIMIT_NICE to allow priorities from 19 to -20
     setrlimit nice 40 40
 
@@ -28,6 +33,10 @@
 on init
     mkdir /mnt/apk 0755 system system
     mkdir /mnt/extra-apk 0755 root root
+
+    # Allow the payload access to the console (default is 0600)
+    chmod 0666 /dev/console
+
     # Microdroid_manager starts apkdmverity/zipfuse/apexd
     start microdroid_manager
 
diff --git a/microdroid/microdroid_group b/microdroid/microdroid_group
new file mode 100644
index 0000000..4eb8fa5
--- /dev/null
+++ b/microdroid/microdroid_group
@@ -0,0 +1 @@
+system_payload::6000:
diff --git a/microdroid/microdroid_passwd b/microdroid/microdroid_passwd
new file mode 100644
index 0000000..bd15182
--- /dev/null
+++ b/microdroid/microdroid_passwd
@@ -0,0 +1 @@
+system_payload_0::6000:6000:::
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index d854d54..fe0cf6a 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -15,6 +15,7 @@
         "android.system.virtualization.payload-rust",
         "libandroid_logger",
         "libanyhow",
+        "libavflog",
         "libapexutil_rust",
         "libapkverify",
         "libbinder_rs",
@@ -31,6 +32,7 @@
         "liblog_rust",
         "libmicrodroid_metadata",
         "libmicrodroid_payload_config",
+        "libmicrodroid_uids",
         "libnix",
         "libonce_cell",
         "libopenssl",
diff --git a/microdroid_manager/microdroid_manager.rc b/microdroid_manager/microdroid_manager.rc
index e257547..da38564 100644
--- a/microdroid_manager/microdroid_manager.rc
+++ b/microdroid_manager/microdroid_manager.rc
@@ -8,8 +8,8 @@
     # TODO(jooyung) remove this when microdroid_manager becomes a daemon
     oneshot
     # CAP_SYS_BOOT is required to exec kexecload from microdroid_manager
-    # CAP_SETCAP is required to allow microdroid_manager to drop capabilities
+    # CAP_SETPCAP is required to allow microdroid_manager to drop capabilities
     #   before executing the payload
-    capabilities AUDIT_CONTROL SYS_ADMIN SYS_BOOT SETPCAP
+    capabilities AUDIT_CONTROL SYS_ADMIN SYS_BOOT SETPCAP SETUID SETGID
     user root
     socket vm_payload_service stream 0666 system system
diff --git a/microdroid_manager/src/dice.rs b/microdroid_manager/src/dice.rs
index 8e078ea..27ec7a5 100644
--- a/microdroid_manager/src/dice.rs
+++ b/microdroid_manager/src/dice.rs
@@ -164,11 +164,11 @@
 /// https://cs.android.com/android/platform/superproject/+/master:hardware/interfaces/security/rkp/aidl/android/hardware/security/keymint/ProtectedData.aidl
 /// {
 ///   -70002: "Microdroid payload",
-///   ? -71000: tstr // payload_config_path
+///   ? -71000: tstr ; payload_config_path
 ///   ? -71001: PayloadConfig
 /// }
 /// PayloadConfig = {
-///   1: tstr // payload_binary_name
+///   1: tstr ; payload_binary_name
 /// }
 pub fn format_payload_config_descriptor(payload: &PayloadMetadata) -> Result<Vec<u8>> {
     const MICRODROID_PAYLOAD_COMPONENT_NAME: &str = "Microdroid payload";
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 319d2df..a48d540 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -528,8 +528,6 @@
 }
 
 impl Zipfuse {
-    const MICRODROID_PAYLOAD_UID: u32 = 0; // TODO(b/264861173) should be non-root
-    const MICRODROID_PAYLOAD_GID: u32 = 0; // TODO(b/264861173) should be non-root
     fn mount(
         &mut self,
         noexec: MountForExec,
@@ -542,9 +540,13 @@
         if let MountForExec::Disallowed = noexec {
             cmd.arg("--noexec");
         }
+        // Let root own the files in APK, so we can access them, but set the group to
+        // allow all payloads to have access too.
+        let (uid, gid) = (microdroid_uids::ROOT_UID, microdroid_uids::MICRODROID_PAYLOAD_GID);
+
         cmd.args(["-p", &ready_prop, "-o", option]);
-        cmd.args(["-u", &Self::MICRODROID_PAYLOAD_UID.to_string()]);
-        cmd.args(["-g", &Self::MICRODROID_PAYLOAD_GID.to_string()]);
+        cmd.args(["-u", &uid.to_string()]);
+        cmd.args(["-g", &gid.to_string()]);
         cmd.arg(zip_path).arg(mount_dir);
         self.ready_properties.push(ready_prop);
         cmd.spawn().with_context(|| format!("Failed to run zipfuse for {mount_dir:?}"))
@@ -850,10 +852,15 @@
 fn exec_task(task: &Task, service: &Strong<dyn IVirtualMachineService>) -> Result<i32> {
     info!("executing main task {:?}...", task);
     let mut command = match task.type_ {
-        TaskType::Executable => Command::new(&task.command),
+        TaskType::Executable => {
+            // TODO(b/296393106): Run system payloads as non-root.
+            Command::new(&task.command)
+        }
         TaskType::MicrodroidLauncher => {
             let mut command = Command::new("/system/bin/microdroid_launcher");
             command.arg(find_library_path(&task.command)?);
+            command.uid(microdroid_uids::MICRODROID_PAYLOAD_UID);
+            command.gid(microdroid_uids::MICRODROID_PAYLOAD_GID);
             command
         }
     };
diff --git a/microdroid_manager/src/vm_payload_service.rs b/microdroid_manager/src/vm_payload_service.rs
index bcddc3a..1e0b574 100644
--- a/microdroid_manager/src/vm_payload_service.rs
+++ b/microdroid_manager/src/vm_payload_service.rs
@@ -18,10 +18,11 @@
 use android_system_virtualization_payload::aidl::android::system::virtualization::payload::IVmPayloadService::{
     BnVmPayloadService, IVmPayloadService, VM_PAYLOAD_SERVICE_SOCKET_NAME};
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
-use anyhow::Result;
-use binder::{Interface, BinderFeatures, ExceptionCode, Status, Strong};
+use anyhow::{anyhow, Context, Result};
+use avflog::LogResult;
+use binder::{Interface, BinderFeatures, ExceptionCode, Strong, IntoBinderResult};
 use diced_open_dice::{DiceArtifacts, OwnedDiceArtifacts};
-use log::{error, info};
+use log::info;
 use rpcbinder::RpcServer;
 use std::os::unix::io::OwnedFd;
 
@@ -39,7 +40,8 @@
 
     fn getVmInstanceSecret(&self, identifier: &[u8], size: i32) -> binder::Result<Vec<u8>> {
         if !(0..=32).contains(&size) {
-            return Err(Status::new_exception(ExceptionCode::ILLEGAL_ARGUMENT, None));
+            return Err(anyhow!("size {size} not in range (0..=32)"))
+                .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT);
         }
         // Use a fixed salt to scope the derivation to this API. It was randomly generated.
         let salt = [
@@ -48,10 +50,10 @@
             0xB7, 0xA8, 0x43, 0x92,
         ];
         let mut secret = vec![0; size.try_into().unwrap()];
-        derive_sealing_key(&self.dice, &salt, identifier, &mut secret).map_err(|e| {
-            error!("Failed to derive VM instance secret: {:?}", e);
-            Status::new_service_specific_error(-1, None)
-        })?;
+        derive_sealing_key(&self.dice, &salt, identifier, &mut secret)
+            .context("Failed to derive VM instance secret")
+            .with_log()
+            .or_service_specific_exception(-1)?;
         Ok(secret)
     }
 
@@ -60,7 +62,7 @@
         if let Some(bcc) = self.dice.bcc() {
             Ok(bcc.to_vec())
         } else {
-            Err(Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some("bcc is none")))
+            Err(anyhow!("bcc is none")).or_binder_exception(ExceptionCode::ILLEGAL_STATE)
         }
     }
 
@@ -91,8 +93,9 @@
         if self.allow_restricted_apis {
             Ok(())
         } else {
-            error!("Use of restricted APIs is not allowed");
-            Err(Status::new_exception_str(ExceptionCode::SECURITY, Some("Use of restricted APIs")))
+            Err(anyhow!("Use of restricted APIs is not allowed"))
+                .with_log()
+                .or_binder_exception(ExceptionCode::SECURITY)
         }
     }
 }
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index bbe00b5..1aa5935 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -7,8 +7,6 @@
     crate_name: "pvmfw",
     defaults: ["vmbase_ffi_defaults"],
     srcs: ["src/main.rs"],
-    // Require unsafe blocks for inside unsafe functions.
-    flags: ["-Dunsafe_op_in_unsafe_fn"],
     features: [
         "legacy",
     ],
diff --git a/pvmfw/README.md b/pvmfw/README.md
index 386036d..698972a 100644
--- a/pvmfw/README.md
+++ b/pvmfw/README.md
@@ -139,6 +139,10 @@
 |  offset = (SECOND - HEAD)     |
 |  size = (SECOND_END - SECOND) |
 +-------------------------------+
+|           [Entry 2]           | <-- Entry 2 is present since version 1.1
+|  offset = (THIRD - HEAD)      |
+|  size = (THIRD_END - SECOND)  |
++-------------------------------+
 |              ...              |
 +-------------------------------+
 |           [Entry n]           |
@@ -152,6 +156,10 @@
 |        {Second blob: DP}      |
 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ <-- SECOND_END
 | (Padding to 8-byte alignment) |
++===============================+ <-- THIRD
+|        {Third blob: VM DTBO}  |
++~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ <-- THIRD_END
+| (Padding to 8-byte alignment) |
 +===============================+
 |              ...              |
 +===============================+ <-- TAIL
@@ -177,6 +185,11 @@
 - entry 1 may point to a [DTBO] to be applied to the pVM device tree. See
   [debug policy][debug_policy] for an example.
 
+In version 1.1, new blob is added.
+
+- entry 2 may point to a [DTBO] that describes VM DTBO for device assignment.
+  pvmfw will provision assigned devices with the VM DTBO.
+
 [header]: src/config.rs
 [DTBO]: https://android.googlesource.com/platform/external/dtc/+/refs/heads/master/Documentation/dt-object-internal.txt
 [debug_policy]: ../docs/debug/README.md#debug-policy
diff --git a/pvmfw/avb/Android.bp b/pvmfw/avb/Android.bp
index 49c4717..4efee6a 100644
--- a/pvmfw/avb/Android.bp
+++ b/pvmfw/avb/Android.bp
@@ -7,8 +7,6 @@
     crate_name: "pvmfw_avb",
     srcs: ["src/lib.rs"],
     prefer_rlib: true,
-    // Require unsafe blocks for inside unsafe functions.
-    flags: ["-Dunsafe_op_in_unsafe_fn"],
     rustlibs: [
         "libavb_bindgen_nostd",
         "libtinyvec_nostd",
diff --git a/pvmfw/src/config.rs b/pvmfw/src/config.rs
index 4086af7..d0a6b7f 100644
--- a/pvmfw/src/config.rs
+++ b/pvmfw/src/config.rs
@@ -18,7 +18,9 @@
 use core::mem;
 use core::ops::Range;
 use core::result;
-use vmbase::util::unchecked_align_up;
+use log::{info, warn};
+use static_assertions::const_assert_eq;
+use vmbase::util::RangeExt;
 use zerocopy::{FromBytes, LayoutVerified};
 
 /// Configuration data header.
@@ -28,13 +30,11 @@
     /// Magic number; must be `Header::MAGIC`.
     magic: u32,
     /// Version of the header format.
-    version: u32,
+    version: Version,
     /// Total size of the configuration data.
     total_size: u32,
     /// Feature flags; currently reserved and must be zero.
     flags: u32,
-    /// (offset, size) pairs used to locate individual entries appended to the header.
-    entries: [HeaderEntry; Entry::COUNT],
 }
 
 #[derive(Debug)]
@@ -46,15 +46,13 @@
     /// Header doesn't contain the expect magic value.
     InvalidMagic,
     /// Version of the header isn't supported.
-    UnsupportedVersion(u16, u16),
-    /// Header sets flags incorrectly or uses reserved flags.
-    InvalidFlags(u32),
+    UnsupportedVersion(Version),
     /// Header describes configuration data that doesn't fit in the expected buffer.
     InvalidSize(usize),
     /// Header entry is missing.
     MissingEntry(Entry),
-    /// Header entry is invalid.
-    InvalidEntry(Entry, EntryError),
+    /// Range described by entry does not fit within config data.
+    EntryOutOfBounds(Entry, Range<usize>, Range<usize>),
 }
 
 impl fmt::Display for Error {
@@ -63,110 +61,69 @@
             Self::BufferTooSmall => write!(f, "Reserved region is smaller than config header"),
             Self::HeaderMisaligned => write!(f, "Reserved region is misaligned"),
             Self::InvalidMagic => write!(f, "Wrong magic number"),
-            Self::UnsupportedVersion(x, y) => write!(f, "Version {x}.{y} not supported"),
-            Self::InvalidFlags(v) => write!(f, "Flags value {v:#x} is incorrect or reserved"),
+            Self::UnsupportedVersion(v) => write!(f, "Version {v} not supported"),
             Self::InvalidSize(sz) => write!(f, "Total size ({sz:#x}) overflows reserved region"),
             Self::MissingEntry(entry) => write!(f, "Mandatory {entry:?} entry is missing"),
-            Self::InvalidEntry(entry, e) => write!(f, "Invalid {entry:?} entry: {e}"),
+            Self::EntryOutOfBounds(entry, range, limits) => {
+                write!(
+                    f,
+                    "Entry {entry:?} out of bounds: {range:#x?} must be within range {limits:#x?}"
+                )
+            }
         }
     }
 }
 
 pub type Result<T> = result::Result<T, Error>;
 
-#[derive(Debug)]
-pub enum EntryError {
-    /// Offset isn't between the fixed minimum value and size of configuration data.
-    InvalidOffset(usize),
-    /// Size must be zero when offset is and not be when it isn't.
-    InvalidSize(usize),
-    /// Entry isn't fully within the configuration data structure.
-    OutOfBounds { offset: usize, size: usize, limit: usize },
-}
-
-impl fmt::Display for EntryError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Self::InvalidOffset(offset) => write!(f, "Invalid offset: {offset:#x?}"),
-            Self::InvalidSize(sz) => write!(f, "Invalid size: {sz:#x?}"),
-            Self::OutOfBounds { offset, size, limit } => {
-                let range = Header::PADDED_SIZE..*limit;
-                let entry = *offset..(*offset + *size);
-                write!(f, "Out of bounds: {entry:#x?} must be within range {range:#x?}")
-            }
-        }
-    }
-}
-
 impl Header {
     const MAGIC: u32 = u32::from_ne_bytes(*b"pvmf");
-    const VERSION_1_0: u32 = Self::version(1, 0);
-    const PADDED_SIZE: usize = unchecked_align_up(mem::size_of::<Self>(), mem::size_of::<u64>());
-
-    pub const fn version(major: u16, minor: u16) -> u32 {
-        ((major as u32) << 16) | (minor as u32)
-    }
-
-    pub const fn version_tuple(&self) -> (u16, u16) {
-        ((self.version >> 16) as u16, self.version as u16)
-    }
+    const VERSION_1_0: Version = Version { major: 1, minor: 0 };
+    const VERSION_1_1: Version = Version { major: 1, minor: 1 };
 
     pub fn total_size(&self) -> usize {
         self.total_size as usize
     }
 
-    pub fn body_size(&self) -> usize {
-        self.total_size() - Self::PADDED_SIZE
+    pub fn body_lowest_bound(&self) -> Result<usize> {
+        let entries_offset = mem::size_of::<Self>();
+
+        // Ensure that the entries are properly aligned and do not require padding.
+        const_assert_eq!(mem::align_of::<Header>() % mem::align_of::<HeaderEntry>(), 0);
+        const_assert_eq!(mem::size_of::<Header>() % mem::align_of::<HeaderEntry>(), 0);
+
+        let entries_size = self.entry_count()?.checked_mul(mem::size_of::<HeaderEntry>()).unwrap();
+
+        Ok(entries_offset.checked_add(entries_size).unwrap())
     }
 
-    fn get_body_range(&self, entry: Entry) -> Result<Option<Range<usize>>> {
-        let e = self.entries[entry as usize];
-        let offset = e.offset as usize;
-        let size = e.size as usize;
-
-        match self._get_body_range(offset, size) {
-            Ok(r) => Ok(r),
-            Err(EntryError::InvalidSize(0)) => {
-                // As our bootloader currently uses this (non-compliant) case, permit it for now.
-                log::warn!("Config entry {entry:?} uses non-zero offset with zero size");
-                // TODO(b/262181812): Either make this case valid or fix the bootloader.
-                Ok(None)
+    pub fn entry_count(&self) -> Result<usize> {
+        let last_entry = match self.version {
+            Self::VERSION_1_0 => Entry::DebugPolicy,
+            Self::VERSION_1_1 => Entry::VmDtbo,
+            v @ Version { major: 1, .. } => {
+                const LATEST: Version = Header::VERSION_1_1;
+                warn!("Parsing unknown config data version {v} as version {LATEST}");
+                return Ok(Entry::COUNT);
             }
-            Err(e) => Err(Error::InvalidEntry(entry, e)),
-        }
-    }
+            v => return Err(Error::UnsupportedVersion(v)),
+        };
 
-    fn _get_body_range(
-        &self,
-        offset: usize,
-        size: usize,
-    ) -> result::Result<Option<Range<usize>>, EntryError> {
-        match (offset, size) {
-            (0, 0) => Ok(None),
-            (0, size) | (_, size @ 0) => Err(EntryError::InvalidSize(size)),
-            _ => {
-                let start = offset
-                    .checked_sub(Header::PADDED_SIZE)
-                    .ok_or(EntryError::InvalidOffset(offset))?;
-                let end = start
-                    .checked_add(size)
-                    .filter(|x| *x <= self.body_size())
-                    .ok_or(EntryError::OutOfBounds { offset, size, limit: self.total_size() })?;
-
-                Ok(Some(start..end))
-            }
-        }
+        Ok(last_entry as usize + 1)
     }
 }
 
 #[derive(Clone, Copy, Debug)]
 pub enum Entry {
-    Bcc = 0,
-    DebugPolicy = 1,
+    Bcc,
+    DebugPolicy,
+    VmDtbo,
+    #[allow(non_camel_case_types)] // TODO: Use mem::variant_count once stable.
+    _VARIANT_COUNT,
 }
 
 impl Entry {
-    const COUNT: usize = 2;
+    const COUNT: usize = Self::_VARIANT_COUNT as usize;
 }
 
 #[repr(packed)]
@@ -176,59 +133,111 @@
     size: u32,
 }
 
+impl HeaderEntry {
+    pub fn as_range(&self) -> Option<Range<usize>> {
+        let size = usize::try_from(self.size).unwrap();
+        if size != 0 {
+            let offset = self.offset.try_into().unwrap();
+            // Allow overflows here for the Range to properly describe the entry (validated later).
+            Some(offset..(offset + size))
+        } else {
+            None
+        }
+    }
+}
+
+#[repr(C, packed)]
+#[derive(Clone, Copy, Debug, Eq, FromBytes, PartialEq)]
+pub struct Version {
+    minor: u16,
+    major: u16,
+}
+
+impl fmt::Display for Version {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        // Copy the fields to local variables to prevent unaligned access.
+        let (major, minor) = (self.major, self.minor);
+        write!(f, "{}.{}", major, minor)
+    }
+}
+
 #[derive(Debug)]
 pub struct Config<'a> {
     body: &'a mut [u8],
-    bcc_range: Range<usize>,
-    dp_range: Option<Range<usize>>,
+    ranges: [Option<Range<usize>>; Entry::COUNT],
 }
 
 impl<'a> Config<'a> {
     /// Take ownership of a pvmfw configuration consisting of its header and following entries.
-    pub fn new(data: &'a mut [u8]) -> Result<Self> {
-        let header = data.get(..Header::PADDED_SIZE).ok_or(Error::BufferTooSmall)?;
+    pub fn new(bytes: &'a mut [u8]) -> Result<Self> {
+        const HEADER_SIZE: usize = mem::size_of::<Header>();
+        if bytes.len() < HEADER_SIZE {
+            return Err(Error::BufferTooSmall);
+        }
 
-        let (header, _) =
-            LayoutVerified::<_, Header>::new_from_prefix(header).ok_or(Error::HeaderMisaligned)?;
+        let (header, rest) =
+            LayoutVerified::<_, Header>::new_from_prefix(bytes).ok_or(Error::HeaderMisaligned)?;
         let header = header.into_ref();
 
         if header.magic != Header::MAGIC {
             return Err(Error::InvalidMagic);
         }
 
-        if header.version != Header::VERSION_1_0 {
-            let (major, minor) = header.version_tuple();
-            return Err(Error::UnsupportedVersion(major, minor));
+        let header_flags = header.flags;
+        if header_flags != 0 {
+            warn!("Ignoring unknown config flags: {header_flags:#x}");
         }
 
-        if header.flags != 0 {
-            return Err(Error::InvalidFlags(header.flags));
-        }
+        info!("pvmfw config version: {}", header.version);
 
-        let bcc_range =
-            header.get_body_range(Entry::Bcc)?.ok_or(Error::MissingEntry(Entry::Bcc))?;
-        let dp_range = header.get_body_range(Entry::DebugPolicy)?;
+        // Validate that we won't get an invalid alignment in the following due to padding to u64.
+        const_assert_eq!(HEADER_SIZE % mem::size_of::<u64>(), 0);
 
-        let body_size = header.body_size();
+        // Ensure that Header::total_size isn't larger than anticipated by the caller and resize
+        // the &[u8] to catch OOB accesses to entries/blobs.
         let total_size = header.total_size();
-        let body = data
-            .get_mut(Header::PADDED_SIZE..)
-            .ok_or(Error::BufferTooSmall)?
-            .get_mut(..body_size)
-            .ok_or(Error::InvalidSize(total_size))?;
+        let rest = if let Some(rest_size) = total_size.checked_sub(HEADER_SIZE) {
+            rest.get_mut(..rest_size).ok_or(Error::InvalidSize(total_size))?
+        } else {
+            return Err(Error::InvalidSize(total_size));
+        };
 
-        Ok(Self { body, bcc_range, dp_range })
+        let (header_entries, body) =
+            LayoutVerified::<_, [HeaderEntry]>::new_slice_from_prefix(rest, header.entry_count()?)
+                .ok_or(Error::BufferTooSmall)?;
+
+        // Validate that we won't get an invalid alignment in the following due to padding to u64.
+        const_assert_eq!(mem::size_of::<HeaderEntry>() % mem::size_of::<u64>(), 0);
+
+        let limits = header.body_lowest_bound()?..total_size;
+        let ranges = [
+            // TODO: Find a way to do this programmatically even if the trait
+            // `core::marker::Copy` is not implemented for `core::ops::Range<usize>`.
+            Self::validated_body_range(Entry::Bcc, &header_entries, &limits)?,
+            Self::validated_body_range(Entry::DebugPolicy, &header_entries, &limits)?,
+            Self::validated_body_range(Entry::VmDtbo, &header_entries, &limits)?,
+        ];
+
+        Ok(Self { body, ranges })
     }
 
     /// Get slice containing the platform BCC.
-    pub fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>) {
-        let bcc_start = self.bcc_range.start;
-        let bcc_end = self.bcc_range.len();
+    pub fn get_entries(&mut self) -> Result<(&mut [u8], Option<&mut [u8]>)> {
+        // This assumes that the blobs are in-order w.r.t. the entries.
+        let bcc_range = self.get_entry_range(Entry::Bcc).ok_or(Error::MissingEntry(Entry::Bcc))?;
+        let dp_range = self.get_entry_range(Entry::DebugPolicy);
+        let vm_dtbo_range = self.get_entry_range(Entry::VmDtbo);
+        // TODO(b/291191157): Provision device assignment with this.
+        if let Some(vm_dtbo_range) = vm_dtbo_range {
+            info!("Found VM DTBO at {:?}", vm_dtbo_range);
+        }
+        let bcc_start = bcc_range.start;
+        let bcc_end = bcc_range.len();
         let (_, rest) = self.body.split_at_mut(bcc_start);
         let (bcc, rest) = rest.split_at_mut(bcc_end);
 
-        let dp = if let Some(dp_range) = &self.dp_range {
-            let dp_start = dp_range.start.checked_sub(self.bcc_range.end).unwrap();
+        let dp = if let Some(dp_range) = dp_range {
+            let dp_start = dp_range.start.checked_sub(bcc_range.end).unwrap();
             let dp_end = dp_range.len();
             let (_, rest) = rest.split_at_mut(dp_start);
             let (dp, _) = rest.split_at_mut(dp_end);
@@ -237,6 +246,31 @@
             None
         };
 
-        (bcc, dp)
+        Ok((bcc, dp))
+    }
+
+    pub fn get_entry_range(&self, entry: Entry) -> Option<Range<usize>> {
+        self.ranges[entry as usize].clone()
+    }
+
+    fn validated_body_range(
+        entry: Entry,
+        header_entries: &[HeaderEntry],
+        limits: &Range<usize>,
+    ) -> Result<Option<Range<usize>>> {
+        if let Some(header_entry) = header_entries.get(entry as usize) {
+            if let Some(r) = header_entry.as_range() {
+                return if r.start <= r.end && r.is_within(limits) {
+                    let start = r.start - limits.start;
+                    let end = r.end - limits.start;
+
+                    Ok(Some(start..end))
+                } else {
+                    Err(Error::EntryOutOfBounds(entry, r, limits.clone()))
+                };
+            }
+        }
+
+        Ok(None)
     }
 }
diff --git a/pvmfw/src/dice.rs b/pvmfw/src/dice.rs
index 28271d3..9542429 100644
--- a/pvmfw/src/dice.rs
+++ b/pvmfw/src/dice.rs
@@ -18,8 +18,8 @@
 use core::mem::size_of;
 use core::slice;
 use diced_open_dice::{
-    bcc_format_config_descriptor, bcc_handover_main_flow, hash, Config, DiceMode, Hash,
-    InputValues, HIDDEN_SIZE,
+    bcc_format_config_descriptor, bcc_handover_main_flow, hash, Config, DiceConfigValues, DiceMode,
+    Hash, InputValues, HIDDEN_SIZE,
 };
 use pvmfw_avb::{DebugLevel, Digest, VerifiedBootData};
 use vmbase::cstr;
@@ -63,12 +63,10 @@
         next_bcc: &mut [u8],
     ) -> diced_open_dice::Result<()> {
         let mut config_descriptor_buffer = [0; 128];
-        let config_descriptor_size = bcc_format_config_descriptor(
-            Some(cstr!("vm_entry")),
-            None,  // component_version
-            false, // resettable
-            &mut config_descriptor_buffer,
-        )?;
+        let config_values =
+            DiceConfigValues { component_name: Some(cstr!("vm_entry")), ..Default::default() };
+        let config_descriptor_size =
+            bcc_format_config_descriptor(&config_values, &mut config_descriptor_buffer)?;
         let config = &config_descriptor_buffer[..config_descriptor_size];
 
         let dice_inputs = InputValues::new(
diff --git a/pvmfw/src/entry.rs b/pvmfw/src/entry.rs
index 9c929a9..3efa61e 100644
--- a/pvmfw/src/entry.rs
+++ b/pvmfw/src/entry.rs
@@ -207,7 +207,10 @@
         RebootReason::InvalidConfig
     })?;
 
-    let (bcc_slice, debug_policy) = appended.get_entries();
+    let (bcc_slice, debug_policy) = appended.get_entries().map_err(|e| {
+        error!("Failed to obtained the config entries: {e}");
+        RebootReason::InvalidConfig
+    })?;
 
     // Up to this point, we were using the built-in static (from .rodata) page tables.
     MEMORY.lock().replace(MemoryTracker::new(
@@ -427,10 +430,10 @@
         }
     }
 
-    fn get_entries(&mut self) -> (&mut [u8], Option<&mut [u8]>) {
+    fn get_entries(&mut self) -> config::Result<(&mut [u8], Option<&mut [u8]>)> {
         match self {
             Self::Config(ref mut cfg) => cfg.get_entries(),
-            Self::LegacyBcc(ref mut bcc) => (bcc, None),
+            Self::LegacyBcc(ref mut bcc) => Ok((bcc, None)),
         }
     }
 }
diff --git a/pvmfw/src/memory.rs b/pvmfw/src/memory.rs
index 27ab719..06158dd 100644
--- a/pvmfw/src/memory.rs
+++ b/pvmfw/src/memory.rs
@@ -14,8 +14,6 @@
 
 //! Low-level allocation and tracking of main memory.
 
-#![deny(unsafe_op_in_unsafe_fn)]
-
 use crate::helpers::PVMFW_PAGE_SIZE;
 use aarch64_paging::paging::VirtualAddress;
 use aarch64_paging::MapError;
diff --git a/rialto/Android.bp b/rialto/Android.bp
index ed9a284..55423ea 100644
--- a/rialto/Android.bp
+++ b/rialto/Android.bp
@@ -9,10 +9,14 @@
     defaults: ["vmbase_ffi_defaults"],
     rustlibs: [
         "libaarch64_paging",
+        "libciborium_io_nostd",
+        "libciborium_nostd",
         "libhyp",
         "libfdtpci",
         "liblibfdt",
         "liblog_rust_nostd",
+        "libservice_vm_comm_nostd",
+        "libtinyvec_nostd",
         "libvirtio_drivers",
         "libvmbase",
     ],
@@ -76,6 +80,7 @@
 }
 
 prebuilt_etc {
+    // rialto_bin is a prebuilt target wrapping the signed bare-metal service VM.
     name: "rialto_bin",
     filename: "rialto.bin",
     target: {
@@ -97,9 +102,11 @@
         "android.system.virtualizationservice-rust",
         "libandroid_logger",
         "libanyhow",
+        "libciborium",
         "liblibc",
         "liblog_rust",
         "libnix",
+        "libservice_vm_comm",
         "libvmclient",
         "libvsock",
     ],
diff --git a/rialto/src/communication.rs b/rialto/src/communication.rs
index f00393d..ee4ecdb 100644
--- a/rialto/src/communication.rs
+++ b/rialto/src/communication.rs
@@ -14,72 +14,193 @@
 
 //! Supports for the communication between rialto and host.
 
-use crate::error::{Error, Result};
+use crate::error::Result;
+use ciborium_io::{Read, Write};
+use core::hint::spin_loop;
+use core::mem;
+use core::result;
 use log::info;
+use service_vm_comm::{Request, Response};
+use tinyvec::ArrayVec;
 use virtio_drivers::{
     self,
     device::socket::{
-        SingleConnectionManager, SocketError, VirtIOSocket, VsockAddr, VsockEventType,
+        SocketError, VirtIOSocket, VsockAddr, VsockConnectionManager, VsockEventType,
     },
     transport::Transport,
     Hal,
 };
 
-const MAX_RECV_BUFFER_SIZE_BYTES: usize = 64;
+const WRITE_BUF_CAPACITY: usize = 512;
 
-pub struct DataChannel<H: Hal, T: Transport> {
-    connection_manager: SingleConnectionManager<H, T>,
+pub struct VsockStream<H: Hal, T: Transport> {
+    connection_manager: VsockConnectionManager<H, T>,
+    /// Peer address. The same port is used on rialto and peer for convenience.
+    peer_addr: VsockAddr,
+    write_buf: ArrayVec<[u8; WRITE_BUF_CAPACITY]>,
 }
 
-impl<H: Hal, T: Transport> From<VirtIOSocket<H, T>> for DataChannel<H, T> {
-    fn from(socket_device_driver: VirtIOSocket<H, T>) -> Self {
-        Self { connection_manager: SingleConnectionManager::new(socket_device_driver) }
+impl<H: Hal, T: Transport> VsockStream<H, T> {
+    pub fn new(
+        socket_device_driver: VirtIOSocket<H, T>,
+        peer_addr: VsockAddr,
+    ) -> virtio_drivers::Result<Self> {
+        let mut vsock_stream = Self {
+            connection_manager: VsockConnectionManager::new(socket_device_driver),
+            peer_addr,
+            write_buf: ArrayVec::default(),
+        };
+        vsock_stream.connect()?;
+        Ok(vsock_stream)
     }
-}
 
-impl<H: Hal, T: Transport> DataChannel<H, T> {
-    /// Connects to the given destination.
-    pub fn connect(&mut self, destination: VsockAddr) -> virtio_drivers::Result {
-        // Use the same port on rialto and host for convenience.
-        self.connection_manager.connect(destination, destination.port)?;
-        self.connection_manager.wait_for_connect()?;
-        info!("Connected to the destination {destination:?}");
+    fn connect(&mut self) -> virtio_drivers::Result {
+        self.connection_manager.connect(self.peer_addr, self.peer_addr.port)?;
+        self.wait_for_connect()?;
+        info!("Connected to the peer {:?}", self.peer_addr);
         Ok(())
     }
 
-    /// Processes the received requests and sends back a reply.
-    pub fn handle_incoming_request(&mut self) -> Result<()> {
-        let mut buffer = [0u8; MAX_RECV_BUFFER_SIZE_BYTES];
-
-        // TODO(b/274441673): Handle the scenario when the given buffer is too short.
-        let len = self.wait_for_recv(&mut buffer).map_err(Error::ReceivingDataFailed)?;
-
-        // TODO(b/291732060): Implement the communication protocol.
-        // Just reverse the received message for now.
-        buffer[..len].reverse();
-        self.connection_manager.send(&buffer[..len])?;
-        Ok(())
-    }
-
-    fn wait_for_recv(&mut self, buffer: &mut [u8]) -> virtio_drivers::Result<usize> {
+    fn wait_for_connect(&mut self) -> virtio_drivers::Result {
         loop {
-            match self.connection_manager.wait_for_recv(buffer)?.event_type {
-                VsockEventType::Disconnected { .. } => {
-                    return Err(SocketError::ConnectionFailed.into())
+            if let Some(event) = self.poll_event_from_peer()? {
+                match event {
+                    VsockEventType::Connected => return Ok(()),
+                    VsockEventType::Disconnected { .. } => {
+                        return Err(SocketError::ConnectionFailed.into())
+                    }
+                    // We shouldn't receive the following event before the connection is
+                    // established.
+                    VsockEventType::ConnectionRequest | VsockEventType::Received { .. } => {
+                        return Err(SocketError::InvalidOperation.into())
+                    }
+                    // We can receive credit requests and updates at any time.
+                    // This can be ignored as the connection manager handles them in poll().
+                    VsockEventType::CreditRequest | VsockEventType::CreditUpdate => {}
                 }
-                VsockEventType::Received { length, .. } => return Ok(length),
-                VsockEventType::Connected
-                | VsockEventType::ConnectionRequest
-                | VsockEventType::CreditRequest
-                | VsockEventType::CreditUpdate => {}
+            } else {
+                spin_loop();
             }
         }
     }
 
+    pub fn read_request(&mut self) -> Result<Request> {
+        Ok(ciborium::from_reader(self)?)
+    }
+
+    pub fn write_response(&mut self, response: &Response) -> Result<()> {
+        Ok(ciborium::into_writer(response, self)?)
+    }
+
     /// Shuts down the data channel.
-    pub fn force_close(&mut self) -> virtio_drivers::Result {
-        self.connection_manager.force_close()?;
+    pub fn shutdown(&mut self) -> virtio_drivers::Result {
+        self.connection_manager.force_close(self.peer_addr, self.peer_addr.port)?;
         info!("Connection shutdown.");
         Ok(())
     }
+
+    fn recv(&mut self, buffer: &mut [u8]) -> virtio_drivers::Result<usize> {
+        self.connection_manager.recv(self.peer_addr, self.peer_addr.port, buffer)
+    }
+
+    fn wait_for_send(&mut self, buffer: &[u8]) -> virtio_drivers::Result {
+        const INSUFFICIENT_BUFFER_SPACE_ERROR: virtio_drivers::Error =
+            virtio_drivers::Error::SocketDeviceError(SocketError::InsufficientBufferSpaceInPeer);
+        loop {
+            match self.connection_manager.send(self.peer_addr, self.peer_addr.port, buffer) {
+                Ok(_) => return Ok(()),
+                Err(INSUFFICIENT_BUFFER_SPACE_ERROR) => {
+                    self.poll()?;
+                }
+                Err(e) => return Err(e),
+            }
+        }
+    }
+
+    fn wait_for_recv(&mut self) -> virtio_drivers::Result {
+        loop {
+            match self.poll()? {
+                Some(VsockEventType::Received { .. }) => return Ok(()),
+                _ => spin_loop(),
+            }
+        }
+    }
+
+    /// Polls the rx queue after the connection is established with the peer, this function
+    /// rejects some invalid events. The valid events are handled inside the connection
+    /// manager.
+    fn poll(&mut self) -> virtio_drivers::Result<Option<VsockEventType>> {
+        if let Some(event) = self.poll_event_from_peer()? {
+            match event {
+                VsockEventType::Disconnected { .. } => Err(SocketError::ConnectionFailed.into()),
+                VsockEventType::Connected | VsockEventType::ConnectionRequest => {
+                    Err(SocketError::InvalidOperation.into())
+                }
+                // When there is a received event, the received data is buffered in the
+                // connection manager's internal receive buffer, so we don't need to do
+                // anything here.
+                // The credit request and updates also handled inside the connection
+                // manager.
+                VsockEventType::Received { .. }
+                | VsockEventType::CreditRequest
+                | VsockEventType::CreditUpdate => Ok(Some(event)),
+            }
+        } else {
+            Ok(None)
+        }
+    }
+
+    fn poll_event_from_peer(&mut self) -> virtio_drivers::Result<Option<VsockEventType>> {
+        Ok(self.connection_manager.poll()?.map(|event| {
+            assert_eq!(event.source, self.peer_addr);
+            assert_eq!(event.destination.port, self.peer_addr.port);
+            event.event_type
+        }))
+    }
+}
+
+impl<H: Hal, T: Transport> Read for VsockStream<H, T> {
+    type Error = virtio_drivers::Error;
+
+    fn read_exact(&mut self, data: &mut [u8]) -> result::Result<(), Self::Error> {
+        let mut start = 0;
+        while start < data.len() {
+            let len = self.recv(&mut data[start..])?;
+            let len = if len == 0 {
+                self.wait_for_recv()?;
+                self.recv(&mut data[start..])?
+            } else {
+                len
+            };
+            start += len;
+        }
+        Ok(())
+    }
+}
+
+impl<H: Hal, T: Transport> Write for VsockStream<H, T> {
+    type Error = virtio_drivers::Error;
+
+    fn write_all(&mut self, data: &[u8]) -> result::Result<(), Self::Error> {
+        if data.len() >= self.write_buf.capacity() - self.write_buf.len() {
+            self.flush()?;
+            if data.len() >= self.write_buf.capacity() {
+                self.wait_for_send(data)?;
+                return Ok(());
+            }
+        }
+        self.write_buf.extend_from_slice(data);
+        Ok(())
+    }
+
+    fn flush(&mut self) -> result::Result<(), Self::Error> {
+        if !self.write_buf.is_empty() {
+            // We need to take the memory from self.write_buf to a temporary
+            // buffer to avoid borrowing `*self` as mutable and immutable on
+            // the same time in `self.wait_for_send(&self.write_buf)`.
+            let buffer = mem::take(&mut self.write_buf);
+            self.wait_for_send(&buffer)?;
+        }
+        Ok(())
+    }
 }
diff --git a/rialto/src/error.rs b/rialto/src/error.rs
index 461870b..23667ed 100644
--- a/rialto/src/error.rs
+++ b/rialto/src/error.rs
@@ -23,7 +23,10 @@
 
 pub type Result<T> = result::Result<T, Error>;
 
-#[derive(Clone, Debug)]
+type CiboriumSerError = ciborium::ser::Error<virtio_drivers::Error>;
+type CiboriumDeError = ciborium::de::Error<virtio_drivers::Error>;
+
+#[derive(Debug)]
 pub enum Error {
     /// Hypervisor error.
     Hypervisor(HypervisorError),
@@ -43,8 +46,10 @@
     MissingVirtIOSocketDevice,
     /// Failed VirtIO driver operation.
     VirtIODriverOperationFailed(virtio_drivers::Error),
-    /// Failed to receive data.
-    ReceivingDataFailed(virtio_drivers::Error),
+    /// Failed to serialize.
+    SerializationFailed(CiboriumSerError),
+    /// Failed to deserialize.
+    DeserializationFailed(CiboriumDeError),
 }
 
 impl fmt::Display for Error {
@@ -65,7 +70,8 @@
             Self::VirtIODriverOperationFailed(e) => {
                 write!(f, "Failed VirtIO driver operation: {e}")
             }
-            Self::ReceivingDataFailed(e) => write!(f, "Failed to receive data: {e}"),
+            Self::SerializationFailed(e) => write!(f, "Failed to serialize: {e}"),
+            Self::DeserializationFailed(e) => write!(f, "Failed to deserialize: {e}"),
         }
     }
 }
@@ -105,3 +111,15 @@
         Self::VirtIODriverOperationFailed(e)
     }
 }
+
+impl From<CiboriumSerError> for Error {
+    fn from(e: CiboriumSerError) -> Self {
+        Self::SerializationFailed(e)
+    }
+}
+
+impl From<CiboriumDeError> for Error {
+    fn from(e: CiboriumDeError) -> Self {
+        Self::DeserializationFailed(e)
+    }
+}
diff --git a/rialto/src/main.rs b/rialto/src/main.rs
index 5c6649a..42d39c4 100644
--- a/rialto/src/main.rs
+++ b/rialto/src/main.rs
@@ -20,11 +20,13 @@
 mod communication;
 mod error;
 mod exceptions;
+mod requests;
 
 extern crate alloc;
 
-use crate::communication::DataChannel;
+use crate::communication::VsockStream;
 use crate::error::{Error, Result};
+use ciborium_io::Write;
 use core::num::NonZeroUsize;
 use core::slice;
 use fdtpci::PciInfo;
@@ -137,10 +139,11 @@
     let socket_device = find_socket_device::<HalImpl>(&mut pci_root)?;
     debug!("Found socket device: guest cid = {:?}", socket_device.guest_cid());
 
-    let mut data_channel = DataChannel::from(socket_device);
-    data_channel.connect(host_addr())?;
-    data_channel.handle_incoming_request()?;
-    data_channel.force_close()?;
+    let mut vsock_stream = VsockStream::new(socket_device, host_addr())?;
+    let response = requests::process_request(vsock_stream.read_request()?);
+    vsock_stream.write_response(&response)?;
+    vsock_stream.flush()?;
+    vsock_stream.shutdown()?;
 
     Ok(())
 }
diff --git a/rialto/src/requests/api.rs b/rialto/src/requests/api.rs
new file mode 100644
index 0000000..11fdde4
--- /dev/null
+++ b/rialto/src/requests/api.rs
@@ -0,0 +1,31 @@
+// Copyright 2023, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! This module contains the main API for the request processing module.
+
+use alloc::vec::Vec;
+use service_vm_comm::{Request, Response};
+
+/// Processes a request and returns the corresponding response.
+/// This function serves as the entry point for the request processing
+/// module.
+pub fn process_request(request: Request) -> Response {
+    match request {
+        Request::Reverse(v) => Response::Reverse(reverse(v)),
+    }
+}
+
+fn reverse(payload: Vec<u8>) -> Vec<u8> {
+    payload.into_iter().rev().collect()
+}
diff --git a/rialto/src/requests/mod.rs b/rialto/src/requests/mod.rs
new file mode 100644
index 0000000..ca22777
--- /dev/null
+++ b/rialto/src/requests/mod.rs
@@ -0,0 +1,19 @@
+// Copyright 2023, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! This module contains functions for the request processing.
+
+mod api;
+
+pub use api::process_request;
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index 8089016..2bd8968 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -22,10 +22,11 @@
     },
     binder::{ParcelFileDescriptor, ProcessState},
 };
-use anyhow::{anyhow, bail, Context, Error};
+use anyhow::{anyhow, bail, Context, Result};
 use log::info;
+use service_vm_comm::{Request, Response};
 use std::fs::File;
-use std::io::{self, BufRead, BufReader, Read, Write};
+use std::io::{self, BufRead, BufReader, BufWriter, Write};
 use std::os::unix::io::FromRawFd;
 use std::panic;
 use std::thread;
@@ -44,7 +45,7 @@
 const INSTANCE_IMG_SIZE: i64 = 1024 * 1024; // 1MB
 
 #[test]
-fn boot_rialto_in_protected_vm_successfully() -> Result<(), Error> {
+fn boot_rialto_in_protected_vm_successfully() -> Result<()> {
     boot_rialto_successfully(
         SIGNED_RIALTO_PATH,
         true, // protected_vm
@@ -52,14 +53,14 @@
 }
 
 #[test]
-fn boot_rialto_in_unprotected_vm_successfully() -> Result<(), Error> {
+fn boot_rialto_in_unprotected_vm_successfully() -> Result<()> {
     boot_rialto_successfully(
         UNSIGNED_RIALTO_PATH,
         false, // protected_vm
     )
 }
 
-fn boot_rialto_successfully(rialto_path: &str, protected_vm: bool) -> Result<(), Error> {
+fn boot_rialto_successfully(rialto_path: &str, protected_vm: bool) -> Result<()> {
     android_logger::init_once(
         android_logger::Config::default().with_tag("rialto").with_min_level(log::Level::Debug),
     );
@@ -169,27 +170,31 @@
     Ok(writer)
 }
 
-fn try_check_socket_connection(port: u32) -> Result<(), Error> {
+fn try_check_socket_connection(port: u32) -> Result<()> {
     info!("Setting up the listening socket on port {port}...");
     let listener = VsockListener::bind_with_cid_port(VMADDR_CID_HOST, port)?;
     info!("Listening on port {port}...");
 
-    let Some(Ok(mut vsock_stream)) = listener.incoming().next() else {
-        bail!("Failed to get vsock_stream");
-    };
+    let mut vsock_stream =
+        listener.incoming().next().ok_or_else(|| anyhow!("Failed to get vsock_stream"))??;
     info!("Accepted connection {:?}", vsock_stream);
-
-    let message = "Hello from host";
-    vsock_stream.write_all(message.as_bytes())?;
-    vsock_stream.flush()?;
-    info!("Sent message: {:?}.", message);
-
-    let mut buffer = vec![0u8; 30];
     vsock_stream.set_read_timeout(Some(Duration::from_millis(1_000)))?;
-    let len = vsock_stream.read(&mut buffer)?;
 
-    assert_eq!(message.len(), len);
-    buffer[..len].reverse();
-    assert_eq!(message.as_bytes(), &buffer[..len]);
+    const WRITE_BUFFER_CAPACITY: usize = 512;
+    let mut buffer = BufWriter::with_capacity(WRITE_BUFFER_CAPACITY, vsock_stream.clone());
+
+    // TODO(b/292080257): Test with message longer than the receiver's buffer capacity
+    // 1024 bytes once the guest virtio-vsock driver fixes the credit update in recv().
+    let message = "abc".repeat(166);
+    let request = Request::Reverse(message.as_bytes().to_vec());
+    ciborium::into_writer(&request, &mut buffer)?;
+    buffer.flush()?;
+    info!("Sent request: {request:?}.");
+
+    let response: Response = ciborium::from_reader(&mut vsock_stream)?;
+    info!("Received response: {response:?}.");
+
+    let expected_response: Vec<u8> = message.as_bytes().iter().rev().cloned().collect();
+    assert_eq!(Response::Reverse(expected_response), response);
     Ok(())
 }
diff --git a/service_vm/client_apk/src/main.rs b/service_vm/client_apk/src/main.rs
index 1f8db96..672dd4a 100644
--- a/service_vm/client_apk/src/main.rs
+++ b/service_vm/client_apk/src/main.rs
@@ -49,12 +49,7 @@
 fn request_certificate(csr: &[u8]) -> Vec<u8> {
     // SAFETY: It is safe as we only request the size of the certificate in this call.
     let certificate_size = unsafe {
-        AVmPayload_requestCertificate(
-            csr.as_ptr() as *const c_void,
-            csr.len(),
-            [].as_mut_ptr() as *mut c_void,
-            0,
-        )
+        AVmPayload_requestCertificate(csr.as_ptr() as *const c_void, csr.len(), [].as_mut_ptr(), 0)
     };
     let mut certificate = vec![0u8; certificate_size];
     // SAFETY: It is safe as we only write the data into the given buffer within the buffer
diff --git a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index 8d467cd..e81f6d7 100644
--- a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -55,6 +55,9 @@
     /** Returns a mask of effective capabilities that the process running the payload binary has. */
     String[] getEffectiveCapabilities();
 
+    /* Return the uid of the process running the binary. */
+    int getUid();
+
     /* write the content into the specified file. */
     void writeToFile(String content, String path);
 
diff --git a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
index f98d1d9..f5656e2 100644
--- a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
+++ b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
@@ -81,12 +81,9 @@
 
     private boolean mNeedTearDown = false;
 
-    private boolean mNeedToRestartPkvmStatus = false;
-
     @Before
     public void setUp() throws Exception {
         mNeedTearDown = false;
-        mNeedToRestartPkvmStatus = false;
 
         assumeDeviceIsCapable(getDevice());
         mNeedTearDown = true;
@@ -104,11 +101,6 @@
             // sees, so we can't rely on that - b/268688303.)
             return;
         }
-        // Restore PKVM status and reboot to prevent previous staged session, if switched.
-        if (mNeedToRestartPkvmStatus) {
-            setPKVMStatusWithRebootToBootloader(true);
-            rebootFromBootloaderAndWaitBootCompleted();
-        }
 
         CommandRunner android = new CommandRunner(getDevice());
 
@@ -117,16 +109,6 @@
     }
 
     @Test
-    public void testBootEnablePKVM() throws Exception {
-        enableDisablePKVMTestHelper(true);
-    }
-
-    @Test
-    public void testBootDisablePKVM() throws Exception {
-        enableDisablePKVMTestHelper(false);
-    }
-
-    @Test
     public void testBootWithCompOS() throws Exception {
         composTestHelper(true);
     }
@@ -424,36 +406,6 @@
         throw new IllegalArgumentException("Failed to get boot time info.");
     }
 
-    private void enableDisablePKVMTestHelper(boolean isEnable) throws Exception {
-        assumePKVMStatusSwitchSupported();
-
-        List<Double> bootDmesgTime = new ArrayList<>(ROUND_COUNT);
-        Map<String, List<Double>> bootloaderTime = new HashMap<>();
-
-        setPKVMStatusWithRebootToBootloader(isEnable);
-        rebootFromBootloaderAndWaitBootCompleted();
-        for (int round = 0; round < ROUND_COUNT; ++round) {
-            getDevice().nonBlockingReboot();
-            waitForBootCompleted();
-
-            updateBootloaderTimeInfo(bootloaderTime);
-
-            double elapsedSec = getDmesgBootTime();
-            bootDmesgTime.add(elapsedSec);
-        }
-
-        String suffix = "";
-        if (isEnable) {
-            suffix = "enable";
-        } else {
-            suffix = "disable";
-        }
-
-        reportMetric(bootDmesgTime, "dmesg_boot_time_with_pkvm_" + suffix, "s");
-        reportAggregatedMetrics(bootloaderTime,
-                "bootloader_time_with_pkvm_" + suffix, "ms");
-    }
-
     private void composTestHelper(boolean isWithCompos) throws Exception {
         assumeFalse("Skip on CF; too slow", isCuttlefish());
 
@@ -481,29 +433,6 @@
         reportMetric(bootDmesgTime, "dmesg_boot_time_" + suffix, "s");
     }
 
-    private void assumePKVMStatusSwitchSupported() throws Exception {
-        assumeFalse("Skip on CF; can't reboot to bootloader", isCuttlefish());
-
-        // This is an overkill. The intention is to exclude remote_device_proxy, which uses
-        // different serial for fastboot. But there's no good way to distinguish from regular IP
-        // transport. This is currently not a problem until someone really needs to run the test
-        // over regular IP transport.
-        assumeFalse("Skip over IP (overkill for remote_device_proxy)", getDevice().isAdbTcp());
-
-        if (!getDevice().isStateBootloaderOrFastbootd()) {
-            getDevice().rebootIntoBootloader();
-        }
-        getDevice().waitForDeviceBootloader();
-
-        CommandResult result;
-        result = getDevice().executeFastbootCommand("oem", "pkvm", "status");
-        rebootFromBootloaderAndWaitBootCompleted();
-        assumeFalse(result.getStderr().contains("Invalid oem command"));
-        // Skip the test if running on a build with pkvm_enabler. Disabling pKVM
-        // for such builds results in a bootloop.
-        assumeTrue(result.getStderr().contains("misc=auto"));
-    }
-
     private void reportMetric(List<Double> data, String name, String unit) {
         CLog.d("Report metric " + name + "(" + unit + ") : " + data.toString());
         Map<String, Double> stats = mMetricsProcessor.computeStats(data, name, unit);
@@ -513,50 +442,6 @@
         }
     }
 
-    private void reportAggregatedMetrics(Map<String, List<Double>> bootloaderTime,
-            String prefix, String unit) {
-
-        for (Map.Entry<String, List<Double>> entry : bootloaderTime.entrySet()) {
-            reportMetric(entry.getValue(), prefix + "_" + entry.getKey(), unit);
-        }
-    }
-
-    private void setPKVMStatusWithRebootToBootloader(boolean isEnable) throws Exception {
-        mNeedToRestartPkvmStatus = true;
-
-        if (!getDevice().isStateBootloaderOrFastbootd()) {
-            getDevice().rebootIntoBootloader();
-        }
-        getDevice().waitForDeviceBootloader();
-
-        CommandResult result;
-        if (isEnable) {
-            result = getDevice().executeFastbootCommand("oem", "pkvm", "enable");
-        } else {
-            result = getDevice().executeFastbootCommand("oem", "pkvm", "disable");
-        }
-
-        result = getDevice().executeFastbootCommand("oem", "pkvm", "status");
-        CLog.i("Gets PKVM status : " + result);
-
-        String expectedOutput = "";
-
-        if (isEnable) {
-            expectedOutput = "pkvm is enabled";
-        } else {
-            expectedOutput = "pkvm is disabled";
-        }
-        assertWithMessage("Failed to set PKVM status. Reason: " + result)
-            .that(result.toString()).ignoringCase().contains(expectedOutput);
-    }
-
-    private void rebootFromBootloaderAndWaitBootCompleted() throws Exception {
-        getDevice().executeFastbootCommand("reboot");
-        getDevice().waitForDeviceOnline(BOOT_COMPLETE_TIMEOUT_MS);
-        getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT_MS);
-        getDevice().enableAdbRoot();
-    }
-
     private void waitForBootCompleted() throws Exception {
         getDevice().waitForDeviceOnline(BOOT_COMPLETE_TIMEOUT_MS);
         getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT_MS);
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 e6d90ea..9f03ab7 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
@@ -451,6 +451,7 @@
         public String mApkContentsPath;
         public String mEncryptedStoragePath;
         public String[] mEffectiveCapabilities;
+        public int mUid;
         public String mFileContent;
         public byte[] mBcc;
         public long[] mTimings;
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java b/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java
index 95eaa58..d752108 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/Pvmfw.java
@@ -33,20 +33,24 @@
     private static final int SIZE_8B = 8; // 8 bytes
     private static final int SIZE_4K = 4 << 10; // 4 KiB, PAGE_SIZE
     private static final int BUFFER_SIZE = 1024;
-    private static final int HEADER_SIZE = Integer.BYTES * 8; // Header has 8 integers.
     private static final int HEADER_MAGIC = 0x666d7670;
-    private static final int HEADER_VERSION = getVersion(1, 0);
+    private static final int HEADER_DEFAULT_VERSION = getVersion(1, 0);
     private static final int HEADER_FLAGS = 0;
 
     @NonNull private final File mPvmfwBinFile;
     @NonNull private final File mBccFile;
     @Nullable private final File mDebugPolicyFile;
+    private final int mVersion;
 
     private Pvmfw(
-            @NonNull File pvmfwBinFile, @NonNull File bccFile, @Nullable File debugPolicyFile) {
+            @NonNull File pvmfwBinFile,
+            @NonNull File bccFile,
+            @Nullable File debugPolicyFile,
+            int version) {
         mPvmfwBinFile = Objects.requireNonNull(pvmfwBinFile);
         mBccFile = Objects.requireNonNull(bccFile);
         mDebugPolicyFile = debugPolicyFile;
+        mVersion = version;
     }
 
     /**
@@ -56,17 +60,22 @@
     public void serialize(@NonNull File outFile) throws IOException {
         Objects.requireNonNull(outFile);
 
-        int bccOffset = HEADER_SIZE;
+        int headerSize = alignTo(getHeaderSize(mVersion), SIZE_8B);
+        int bccOffset = headerSize;
         int bccSize = (int) mBccFile.length();
 
         int debugPolicyOffset = alignTo(bccOffset + bccSize, SIZE_8B);
         int debugPolicySize = mDebugPolicyFile == null ? 0 : (int) mDebugPolicyFile.length();
 
         int totalSize = debugPolicyOffset + debugPolicySize;
+        if (hasVmDtbo(mVersion)) {
+            // Add VM DTBO size as well.
+            totalSize += Integer.BYTES * 2;
+        }
 
-        ByteBuffer header = ByteBuffer.allocate(HEADER_SIZE).order(LITTLE_ENDIAN);
+        ByteBuffer header = ByteBuffer.allocate(headerSize).order(LITTLE_ENDIAN);
         header.putInt(HEADER_MAGIC);
-        header.putInt(HEADER_VERSION);
+        header.putInt(mVersion);
         header.putInt(totalSize);
         header.putInt(HEADER_FLAGS);
         header.putInt(bccOffset);
@@ -74,11 +83,18 @@
         header.putInt(debugPolicyOffset);
         header.putInt(debugPolicySize);
 
+        if (hasVmDtbo(mVersion)) {
+            // Add placeholder entry for VM DTBO.
+            // TODO(b/291191157): Add a real DTBO and test.
+            header.putInt(0);
+            header.putInt(0);
+        }
+
         try (FileOutputStream pvmfw = new FileOutputStream(outFile)) {
             appendFile(pvmfw, mPvmfwBinFile);
             padTo(pvmfw, SIZE_4K);
             pvmfw.write(header.array());
-            padTo(pvmfw, HEADER_SIZE);
+            padTo(pvmfw, SIZE_8B);
             appendFile(pvmfw, mBccFile);
             if (mDebugPolicyFile != null) {
                 padTo(pvmfw, SIZE_8B);
@@ -110,6 +126,19 @@
         }
     }
 
+    private static int getHeaderSize(int version) {
+        if (version == getVersion(1, 0)) {
+            return Integer.BYTES * 8; // Header has 8 integers.
+        }
+        return Integer.BYTES * 10; // Default + VM DTBO (offset, size)
+    }
+
+    private static boolean hasVmDtbo(int version) {
+        int major = getMajorVersion(version);
+        int minor = getMinorVersion(version);
+        return major > 1 || (major == 1 && minor >= 1);
+    }
+
     private static int alignTo(int x, int size) {
         return (x + size - 1) & ~(size - 1);
     }
@@ -118,15 +147,25 @@
         return ((major & 0xFFFF) << 16) | (minor & 0xFFFF);
     }
 
+    private static int getMajorVersion(int version) {
+        return (version >> 16) & 0xFFFF;
+    }
+
+    private static int getMinorVersion(int version) {
+        return version & 0xFFFF;
+    }
+
     /** Builder for {@link Pvmfw}. */
     public static final class Builder {
         @NonNull private final File mPvmfwBinFile;
         @NonNull private final File mBccFile;
         @Nullable private File mDebugPolicyFile;
+        private int mVersion;
 
         public Builder(@NonNull File pvmfwBinFile, @NonNull File bccFile) {
             mPvmfwBinFile = Objects.requireNonNull(pvmfwBinFile);
             mBccFile = Objects.requireNonNull(bccFile);
+            mVersion = HEADER_DEFAULT_VERSION;
         }
 
         @NonNull
@@ -136,8 +175,14 @@
         }
 
         @NonNull
+        public Builder setVersion(int major, int minor) {
+            mVersion = getVersion(major, minor);
+            return this;
+        }
+
+        @NonNull
         public Pvmfw build() {
-            return new Pvmfw(mPvmfwBinFile, mBccFile, mDebugPolicyFile);
+            return new Pvmfw(mPvmfwBinFile, mBccFile, mDebugPolicyFile, mVersion);
         }
     }
 }
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 82d8571..21960b4 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -1065,6 +1065,27 @@
         }
     }
 
+    @Test
+    public void testDevcieAssignment() throws Exception {
+        assumeProtectedVmSupported();
+        assumeVfioPlatformSupported();
+
+        List<String> devices = getAssignableDevices();
+        assumeFalse("no assignable devices", devices.isEmpty());
+
+        final String configPath = "assets/vm_config.json";
+        mMicrodroidDevice =
+                MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
+                        .debugLevel("full")
+                        .memoryMib(minMemorySize())
+                        .cpuTopology("match_host")
+                        .protectedVm(true)
+                        .addAssignableDevice(devices.get(0))
+                        .build(getAndroidDevice());
+
+        mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
+    }
+
     @Before
     public void setUp() throws Exception {
         assumeDeviceIsCapable(getDevice());
@@ -1110,6 +1131,31 @@
                 getAndroidDevice().supportsMicrodroid(false));
     }
 
+    private void assumeVfioPlatformSupported() throws Exception {
+        TestDevice device = getAndroidDevice();
+        assumeTrue(
+                "Test skipped because VFIO platform is not supported.",
+                device.doesFileExist("/dev/vfio/vfio")
+                        && device.doesFileExist("/sys/bus/platform/drivers/vfio-platform"));
+    }
+
+    private List<String> getAssignableDevices() throws Exception {
+        CommandRunner android = new CommandRunner(getDevice());
+        String result = android.run("/apex/com.android.virt/bin/vm", "info");
+        List<String> devices = new ArrayList<>();
+        for (String line : result.split("\n")) {
+            final String header = "Assignable devices: ";
+            if (!line.startsWith(header)) continue;
+
+            JSONArray jsonArray = new JSONArray(line.substring(header.length()));
+            for (int i = 0; i < jsonArray.length(); i++) {
+                devices.add(jsonArray.getString(i));
+            }
+            break;
+        }
+        return devices;
+    }
+
     private TestDevice getAndroidDevice() {
         TestDevice androidDevice = (TestDevice) getDevice();
         assertThat(androidDevice).isNotNull();
diff --git a/tests/hostside/java/com/android/microdroid/test/PvmfwImgTest.java b/tests/hostside/java/com/android/microdroid/test/PvmfwImgTest.java
new file mode 100644
index 0000000..320b722
--- /dev/null
+++ b/tests/hostside/java/com/android/microdroid/test/PvmfwImgTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.microdroid.test;
+
+import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeTrue;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assert.assertThrows;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
+import com.android.microdroid.test.host.Pvmfw;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceRuntimeException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.TestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.Objects;
+
+/** Tests pvmfw.img and pvmfw */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class PvmfwImgTest extends MicrodroidHostTestCaseBase {
+    @NonNull private static final String PVMFW_FILE_NAME = "pvmfw_test.bin";
+    @NonNull private static final String BCC_FILE_NAME = "bcc.dat";
+    @NonNull private static final String PACKAGE_FILE_NAME = "MicrodroidTestApp.apk";
+    @NonNull private static final String PACKAGE_NAME = "com.android.microdroid.test";
+    @NonNull private static final String MICRODROID_DEBUG_FULL = "full";
+    @NonNull private static final String MICRODROID_CONFIG_PATH = "assets/vm_config_apex.json";
+    private static final int BOOT_COMPLETE_TIMEOUT_MS = 30000; // 30 seconds
+    private static final int BOOT_FAILURE_WAIT_TIME_MS = 10000; // 10 seconds
+
+    @NonNull private static final String CUSTOM_PVMFW_FILE_PREFIX = "pvmfw";
+    @NonNull private static final String CUSTOM_PVMFW_FILE_SUFFIX = ".bin";
+    @NonNull private static final String CUSTOM_PVMFW_IMG_PATH = TEST_ROOT + PVMFW_FILE_NAME;
+    @NonNull private static final String CUSTOM_PVMFW_IMG_PATH_PROP = "hypervisor.pvmfw.path";
+
+    @Nullable private static File mPvmfwBinFileOnHost;
+    @Nullable private static File mBccFileOnHost;
+
+    @Nullable private TestDevice mAndroidDevice;
+    @Nullable private ITestDevice mMicrodroidDevice;
+    @Nullable private File mCustomPvmfwBinFileOnHost;
+
+    @Before
+    public void setUp() throws Exception {
+        mAndroidDevice = (TestDevice) Objects.requireNonNull(getDevice());
+
+        // Check device capabilities
+        assumeDeviceIsCapable(mAndroidDevice);
+        assumeTrue(
+                "Skip if protected VMs are not supported",
+                mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true));
+        assumeFalse("Test requires setprop for using custom pvmfw and adb root", isUserBuild());
+
+        assumeTrue("Skip if adb root fails", mAndroidDevice.enableAdbRoot());
+
+        // tradefed copies the test artfacts under /tmp when running tests,
+        // so we should *find* the artifacts with the file name.
+        mPvmfwBinFileOnHost =
+                getTestInformation().getDependencyFile(PVMFW_FILE_NAME, /* targetFirst= */ false);
+        mBccFileOnHost =
+                getTestInformation().getDependencyFile(BCC_FILE_NAME, /* targetFirst= */ false);
+
+        // Prepare for system properties for custom pvmfw.img.
+        // File will be prepared later in individual test and then pushed to device
+        // when launching with launchProtectedVmAndWaitForBootCompleted().
+        mCustomPvmfwBinFileOnHost =
+                FileUtil.createTempFile(CUSTOM_PVMFW_FILE_PREFIX, CUSTOM_PVMFW_FILE_SUFFIX);
+        mAndroidDevice.setProperty(CUSTOM_PVMFW_IMG_PATH_PROP, CUSTOM_PVMFW_IMG_PATH);
+
+        // Prepare for launching microdroid
+        mAndroidDevice.installPackage(findTestFile(PACKAGE_FILE_NAME), /* reinstall */ false);
+        prepareVirtualizationTestSetup(mAndroidDevice);
+        mMicrodroidDevice = null;
+    }
+
+    @After
+    public void shutdown() throws Exception {
+        if (!mAndroidDevice.supportsMicrodroid(/* protectedVm= */ true)) {
+            return;
+        }
+        if (mMicrodroidDevice != null) {
+            mAndroidDevice.shutdownMicrodroid(mMicrodroidDevice);
+            mMicrodroidDevice = null;
+        }
+        mAndroidDevice.uninstallPackage(PACKAGE_NAME);
+
+        // Cleanup for custom pvmfw.img
+        mAndroidDevice.setProperty(CUSTOM_PVMFW_IMG_PATH_PROP, "");
+        FileUtil.deleteFile(mCustomPvmfwBinFileOnHost);
+
+        cleanUpVirtualizationTestSetup(mAndroidDevice);
+
+        mAndroidDevice.disableAdbRoot();
+    }
+
+    @Test
+    public void testConfigVersion1_0_boots() throws Exception {
+        Pvmfw pvmfw =
+                new Pvmfw.Builder(mPvmfwBinFileOnHost, mBccFileOnHost).setVersion(1, 0).build();
+        pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+        launchProtectedVmAndWaitForBootCompleted(BOOT_COMPLETE_TIMEOUT_MS);
+    }
+
+    @Test
+    public void testConfigVersion1_1_boots() throws Exception {
+        Pvmfw pvmfw =
+                new Pvmfw.Builder(mPvmfwBinFileOnHost, mBccFileOnHost).setVersion(1, 1).build();
+        pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+        launchProtectedVmAndWaitForBootCompleted(BOOT_COMPLETE_TIMEOUT_MS);
+    }
+
+    @Test
+    public void testInvalidConfigVersion_doesNotBoot() throws Exception {
+        // Disclaimer: Update versions when it becomes valid
+        Pvmfw pvmfw =
+                new Pvmfw.Builder(mPvmfwBinFileOnHost, mBccFileOnHost).setVersion(2, 0).build();
+        pvmfw.serialize(mCustomPvmfwBinFileOnHost);
+
+        assertThrows(
+                "pvmfw shouldn't boot with invalid version",
+                DeviceRuntimeException.class,
+                () -> launchProtectedVmAndWaitForBootCompleted(BOOT_FAILURE_WAIT_TIME_MS));
+    }
+
+    private ITestDevice launchProtectedVmAndWaitForBootCompleted(long adbTimeoutMs)
+            throws DeviceNotAvailableException {
+        mMicrodroidDevice =
+                MicrodroidBuilder.fromDevicePath(
+                                getPathForPackage(PACKAGE_NAME), MICRODROID_CONFIG_PATH)
+                        .debugLevel(MICRODROID_DEBUG_FULL)
+                        .protectedVm(true)
+                        .addBootFile(mCustomPvmfwBinFileOnHost, PVMFW_FILE_NAME)
+                        .setAdbConnectTimeoutMs(adbTimeoutMs)
+                        .build(mAndroidDevice);
+        assertThat(mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT_MS)).isTrue();
+        return mMicrodroidDevice;
+    }
+}
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 8a31c21..526f240 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -2,6 +2,17 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+android_app_certificate {
+    name: "MicrodroidTestAppCert",
+
+    // The default app cert is the same as the default platform cert
+    // (on a test-keys build), which means we end up getting assigned
+    // the permissions via signature and can't reliably disclaim
+    // them. So instead we use our own custom cert. See b/290582742.
+    // Created via: development/tools/make_key microdroid_test_app '/CN=microdroid_test_app'
+    certificate: "microdroid_test_app",
+}
+
 java_defaults {
     name: "MicrodroidTestAppsDefaults",
     test_suites: [
@@ -12,6 +23,7 @@
         "com.android.microdroid.testservice-java",
         "com.android.microdroid.test.vmshare_service-java",
     ],
+    certificate: ":MicrodroidTestAppCert",
     sdk_version: "test_current",
     jni_uses_platform_apis: true,
     use_embedded_native_libs: true,
diff --git a/tests/testapk/microdroid_test_app.pk8 b/tests/testapk/microdroid_test_app.pk8
new file mode 100644
index 0000000..dc012bd
--- /dev/null
+++ b/tests/testapk/microdroid_test_app.pk8
Binary files differ
diff --git a/tests/testapk/microdroid_test_app.x509.pem b/tests/testapk/microdroid_test_app.x509.pem
new file mode 100644
index 0000000..9a0309c
--- /dev/null
+++ b/tests/testapk/microdroid_test_app.x509.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDHzCCAgegAwIBAgIUNnOI4tOMieX67OtyD+6BjTsLm0IwDQYJKoZIhvcNAQEL
+BQAwHjEcMBoGA1UEAwwTbWljcm9kcm9pZF90ZXN0X2FwcDAgFw0yMzA4MTgxNDA4
+MDZaGA8yMDUxMDEwMzE0MDgwNlowHjEcMBoGA1UEAwwTbWljcm9kcm9pZF90ZXN0
+X2FwcDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK7B9xDTD2kS4xFQ
+gwQThRqnxKzOmckYqv2XznXq7tCuhU+RgXDrub7Aiq+QgA25Ouw8ORM5FkZAxD6j
+hCRSVo8cyXdNfPygRY/56umL6KqLMqB0tXLHPst3Lh8fl2su2S+jWL71lUwdOBmu
+nBIa1UqxI9PChR/uIqGyDxNRlUnqOA5/FgyX95P9wj8zmXEFe5No8rL/9hjpBvw1
+cOJCH4hea6JKDA15XYxDaTyj5pkmGb228ZbQb10XwOIhtS94CVxIvqmREzZHL7b0
+cjzCwFDDF6sQoVDi71eFYSWInxSNErDU6wv5h2t6+PV+9mGwTi/AJuxTmevSUoAp
+tGwq0NMCAwEAAaNTMFEwHQYDVR0OBBYEFI2m/0SoaNew99YPQlo6oYPJfh7lMB8G
+A1UdIwQYMBaAFI2m/0SoaNew99YPQlo6oYPJfh7lMA8GA1UdEwEB/wQFMAMBAf8w
+DQYJKoZIhvcNAQELBQADggEBABxIQ66ACIrSnDCiI/DqdPPwHf4vva2Y0bVJ5tXN
+ufFQN0Hr4UnttDzWPtfZHQTnrA478b9Z/g4Y0qg/tj2g5oZP50coF9a39mPe6v2k
+vazkMp2H/+ilG4c8L6QsC7UKXn7Lxxznn3ijlh1lYVJ3E6nMibGRKrfaVFpEwtvy
+zT0K8eK9KUZIyG5nf1v8On4Vfu7MnavuxNubKoUhfu0B8hSd5JKiGDuUkSk3MiFX
+uctYmJZEUD1xLI787SzqrhuYMGfuwmrrI0N46yvUgRgxpkVj2s6GNWqRD3F/fOG+
+qFbeenHjFoMJN9HIAZaz4OqzgGfhfMf596rn+HPAJnRMtsI=
+-----END CERTIFICATE-----
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 f6dc1b8..a928dcf 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -72,6 +72,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.function.ThrowingRunnable;
@@ -1523,6 +1524,30 @@
     }
 
     @Test
+    @Ignore // Figure out how to run this conditionally
+    @CddTest(requirements = {"9.17/C-1-1"})
+    public void payloadIsNotRoot() throws Exception {
+        assumeSupportedDevice();
+
+        VirtualMachineConfig config =
+                newVmConfigBuilder()
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setMemoryBytes(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mUid = ts.getUid();
+                        });
+        testResults.assertNoException();
+        assertThat(testResults.mUid).isNotEqualTo(0);
+    }
+
+    @Test
     @CddTest(requirements = {"9.17/C-1-1"})
     public void encryptedStorageIsPersistent() throws Exception {
         assumeSupportedDevice();
@@ -1971,8 +1996,12 @@
                         | OsConstants.S_IROTH
                         | OsConstants.S_IWOTH
                         | OsConstants.S_IXOTH;
-        assertThat(testResults.mFileMode & allPermissionsMask)
-                .isEqualTo(OsConstants.S_IRUSR | OsConstants.S_IXUSR);
+        int expectedPermissions =
+                OsConstants.S_IRUSR
+                        | OsConstants.S_IXUSR
+                        | OsConstants.S_IRGRP
+                        | OsConstants.S_IXGRP;
+        assertThat(testResults.mFileMode & allPermissionsMask).isEqualTo(expectedPermissions);
     }
 
     // Taken from bionic/libc/kernel/uapi/linux/mount.h
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 297b505..c9b5e3a 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -248,6 +248,11 @@
             return ScopedAStatus::ok();
         }
 
+        ScopedAStatus getUid(int* out) override {
+            *out = getuid();
+            return ScopedAStatus::ok();
+        }
+
         ScopedAStatus runEchoReverseServer() override {
             auto result = start_echo_reverse_server();
             if (result.ok()) {
diff --git a/tests/testapk/test.keystore b/tests/testapk/test.keystore
deleted file mode 100644
index 2946641..0000000
--- a/tests/testapk/test.keystore
+++ /dev/null
Binary files differ
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
index 0ddf70b..dc8908b 100644
--- a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -220,6 +220,11 @@
         }
 
         @Override
+        public int getUid() throws RemoteException {
+            throw new UnsupportedOperationException("Not supported");
+        }
+
+        @Override
         public void writeToFile(String content, String path) throws RemoteException {
             throw new UnsupportedOperationException("Not supported");
         }
diff --git a/virtualizationmanager/Android.bp b/virtualizationmanager/Android.bp
index 59e507f..de39aa2 100644
--- a/virtualizationmanager/Android.bp
+++ b/virtualizationmanager/Android.bp
@@ -27,6 +27,7 @@
         "libandroid_logger",
         "libanyhow",
         "libapkverify",
+        "libavflog",
         "libbase_rust",
         "libbinder_rs",
         "libclap",
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 91bd60b..97151d7 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -50,9 +50,11 @@
 };
 use anyhow::{anyhow, bail, Context, Result};
 use apkverify::{HashAlgorithm, V4Signature};
+use avflog::LogResult;
 use binder::{
     self, wait_for_interface, BinderFeatures, ExceptionCode, Interface, ParcelFileDescriptor,
     Status, StatusCode, Strong,
+    IntoBinderResult,
 };
 use disk::QcowFile;
 use lazy_static::lazy_static;
@@ -179,7 +181,6 @@
         Ok(())
     }
 }
-
 impl IVirtualizationService for VirtualizationService {
     /// Creates (but does not start) a new VM with the given configuration, assigning it the next
     /// available CID.
@@ -212,27 +213,17 @@
         partition_type: PartitionType,
     ) -> binder::Result<()> {
         check_manage_access()?;
-        let size_bytes = size_bytes.try_into().map_err(|e| {
-            Status::new_exception_str(
-                ExceptionCode::ILLEGAL_ARGUMENT,
-                Some(format!("Invalid size {}: {:?}", size_bytes, e)),
-            )
-        })?;
+        let size_bytes = size_bytes
+            .try_into()
+            .with_context(|| format!("Invalid size: {}", size_bytes))
+            .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?;
         let size_bytes = round_up(size_bytes, PARTITION_GRANULARITY_BYTES);
         let image = clone_file(image_fd)?;
         // initialize the file. Any data in the file will be erased.
-        image.set_len(0).map_err(|e| {
-            Status::new_service_specific_error_str(
-                -1,
-                Some(format!("Failed to reset a file: {:?}", e)),
-            )
-        })?;
-        let mut part = QcowFile::new(image, size_bytes).map_err(|e| {
-            Status::new_service_specific_error_str(
-                -1,
-                Some(format!("Failed to create QCOW2 image: {:?}", e)),
-            )
-        })?;
+        image.set_len(0).context("Failed to reset a file").or_service_specific_exception(-1)?;
+        let mut part = QcowFile::new(image, size_bytes)
+            .context("Failed to create QCOW2 image")
+            .or_service_specific_exception(-1)?;
 
         match partition_type {
             PartitionType::RAW => Ok(()),
@@ -243,12 +234,8 @@
                 format!("Unsupported partition type {:?}", partition_type),
             )),
         }
-        .map_err(|e| {
-            Status::new_service_specific_error_str(
-                -1,
-                Some(format!("Failed to initialize partition as {:?}: {:?}", partition_type, e)),
-            )
-        })?;
+        .with_context(|| format!("Failed to initialize partition as {:?}", partition_type))
+        .or_service_specific_exception(-1)?;
 
         Ok(())
     }
@@ -261,8 +248,7 @@
     ) -> binder::Result<()> {
         check_manage_access()?;
 
-        create_or_update_idsig_file(input_fd, idsig_fd)
-            .map_err(|e| Status::new_service_specific_error_str(-1, Some(format!("{:?}", e))))?;
+        create_or_update_idsig_file(input_fd, idsig_fd).or_service_specific_exception(-1)?;
         Ok(())
     }
 
@@ -309,10 +295,8 @@
                 }
             }
         }
-        Err(Status::new_service_specific_error_str(
-            -1,
-            Some("Too many attempts to create VM context failed."),
-        ))
+        Err(anyhow!("Too many attempts to create VM context failed"))
+            .or_service_specific_exception(-1)
     }
 
     fn create_vm_internal(
@@ -381,12 +365,12 @@
         let (is_app_config, config) = match config {
             VirtualMachineConfig::RawConfig(config) => (false, BorrowedOrOwned::Borrowed(config)),
             VirtualMachineConfig::AppConfig(config) => {
-                let config =
-                    load_app_config(config, &debug_config, &temporary_directory).map_err(|e| {
+                let config = load_app_config(config, &debug_config, &temporary_directory)
+                    .or_service_specific_exception_with(-1, |e| {
                         *is_protected = config.protectedVm;
                         let message = format!("Failed to load app config: {:?}", e);
                         error!("{}", message);
-                        Status::new_service_specific_error_str(-1, Some(message))
+                        message
                     })?;
                 (true, BorrowedOrOwned::Owned(config))
             }
@@ -410,26 +394,21 @@
                 }
             })
             .try_for_each(check_label_for_partition)
-            .map_err(|e| Status::new_service_specific_error_str(-1, Some(format!("{:?}", e))))?;
+            .or_service_specific_exception(-1)?;
 
         let kernel = maybe_clone_file(&config.kernel)?;
         let initrd = maybe_clone_file(&config.initrd)?;
 
         // In a protected VM, we require custom kernels to come from a trusted source (b/237054515).
         if config.protectedVm {
-            check_label_for_kernel_files(&kernel, &initrd).map_err(|e| {
-                Status::new_service_specific_error_str(-1, Some(format!("{:?}", e)))
-            })?;
+            check_label_for_kernel_files(&kernel, &initrd).or_service_specific_exception(-1)?;
         }
 
         let zero_filler_path = temporary_directory.join("zero.img");
-        write_zero_filler(&zero_filler_path).map_err(|e| {
-            error!("Failed to make composite image: {:?}", e);
-            Status::new_service_specific_error_str(
-                -1,
-                Some(format!("Failed to make composite image: {:?}", e)),
-            )
-        })?;
+        write_zero_filler(&zero_filler_path)
+            .context("Failed to make composite image")
+            .with_log()
+            .or_service_specific_exception(-1)?;
 
         // Assemble disk images if needed.
         let disks = config
@@ -450,28 +429,21 @@
             CpuTopology::MATCH_HOST => (None, true),
             CpuTopology::ONE_CPU => (NonZeroU32::new(1), false),
             val => {
-                error!("Unexpected value of CPU topology: {:?}", val);
-                return Err(Status::new_service_specific_error_str(
-                    -1,
-                    Some(format!("Failed to parse CPU topology value: {:?}", val)),
-                ));
+                return Err(anyhow!("Failed to parse CPU topology value {:?}", val))
+                    .with_log()
+                    .or_service_specific_exception(-1);
             }
         };
 
         let devices_dtbo = if !config.devices.is_empty() {
             let mut set = HashSet::new();
             for device in config.devices.iter() {
-                let path = canonicalize(device).map_err(|e| {
-                    Status::new_exception_str(
-                        ExceptionCode::ILLEGAL_ARGUMENT,
-                        Some(format!("can't canonicalize {device}: {e:?}")),
-                    )
-                })?;
+                let path = canonicalize(device)
+                    .with_context(|| format!("can't canonicalize {device}"))
+                    .or_service_specific_exception(-1)?;
                 if !set.insert(path) {
-                    return Err(Status::new_exception_str(
-                        ExceptionCode::ILLEGAL_ARGUMENT,
-                        Some(format!("duplicated device {device}")),
-                    ));
+                    return Err(anyhow!("duplicated device {device}"))
+                        .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT);
                 }
             }
             let dtbo_path = temporary_directory.join("dtbo");
@@ -533,13 +505,9 @@
                 requester_debug_pid,
                 vm_context,
             )
-            .map_err(|e| {
-                error!("Failed to create VM with config {:?}: {:?}", config, e);
-                Status::new_service_specific_error_str(
-                    -1,
-                    Some(format!("Failed to create VM: {:?}", e)),
-                )
-            })?,
+            .with_context(|| format!("Failed to create VM with config {:?}", config))
+            .with_log()
+            .or_service_specific_exception(-1)?,
         );
         state.add_vm(Arc::downgrade(&instance));
         Ok(VirtualMachine::create(instance))
@@ -590,10 +558,8 @@
     let image = if !disk.partitions.is_empty() {
         if disk.image.is_some() {
             warn!("DiskImage {:?} contains both image and partitions.", disk);
-            return Err(Status::new_exception_str(
-                ExceptionCode::ILLEGAL_ARGUMENT,
-                Some("DiskImage contains both image and partitions."),
-            ));
+            return Err(anyhow!("DiskImage contains both image and partitions"))
+                .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT);
         }
 
         let composite_image_filenames =
@@ -605,13 +571,9 @@
             &composite_image_filenames.header,
             &composite_image_filenames.footer,
         )
-        .map_err(|e| {
-            error!("Failed to make composite image with config {:?}: {:?}", disk, e);
-            Status::new_service_specific_error_str(
-                -1,
-                Some(format!("Failed to make composite image: {:?}", e)),
-            )
-        })?;
+        .with_context(|| format!("Failed to make composite disk image with config {:?}", disk))
+        .with_log()
+        .or_service_specific_exception(-1)?;
 
         // Pass the file descriptors for the various partition files to crosvm when it
         // is run.
@@ -622,10 +584,8 @@
         clone_file(image)?
     } else {
         warn!("DiskImage {:?} didn't contain image or partitions.", disk);
-        return Err(Status::new_exception_str(
-            ExceptionCode::ILLEGAL_ARGUMENT,
-            Some("DiskImage didn't contain image or partitions."),
-        ));
+        return Err(anyhow!("DiskImage didn't contain image or partitions."))
+            .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT);
     };
 
     Ok(DiskFile { image, writable: disk.writable })
@@ -783,10 +743,8 @@
     if perm_svc.checkPermission(perm, calling_pid, calling_uid as i32)? {
         Ok(())
     } else {
-        Err(Status::new_exception_str(
-            ExceptionCode::SECURITY,
-            Some(format!("does not have the {} permission", perm)),
-        ))
+        Err(anyhow!("does not have the {} permission", perm))
+            .or_binder_exception(ExceptionCode::SECURITY)
     }
 }
 
@@ -892,40 +850,41 @@
     }
 
     fn start(&self) -> binder::Result<()> {
-        self.instance.start().map_err(|e| {
-            error!("Error starting VM with CID {}: {:?}", self.instance.cid, e);
-            Status::new_service_specific_error_str(-1, Some(e.to_string()))
-        })
+        self.instance
+            .start()
+            .with_context(|| format!("Error starting VM with CID {}", self.instance.cid))
+            .with_log()
+            .or_service_specific_exception(-1)
     }
 
     fn stop(&self) -> binder::Result<()> {
-        self.instance.kill().map_err(|e| {
-            error!("Error stopping VM with CID {}: {:?}", self.instance.cid, e);
-            Status::new_service_specific_error_str(-1, Some(e.to_string()))
-        })
+        self.instance
+            .kill()
+            .with_context(|| format!("Error stopping VM with CID {}", self.instance.cid))
+            .with_log()
+            .or_service_specific_exception(-1)
     }
 
     fn onTrimMemory(&self, level: MemoryTrimLevel) -> binder::Result<()> {
-        self.instance.trim_memory(level).map_err(|e| {
-            error!("Error trimming VM with CID {}: {:?}", self.instance.cid, e);
-            Status::new_service_specific_error_str(-1, Some(e.to_string()))
-        })
+        self.instance
+            .trim_memory(level)
+            .with_context(|| format!("Error trimming VM with CID {}", self.instance.cid))
+            .with_log()
+            .or_service_specific_exception(-1)
     }
 
     fn connectVsock(&self, port: i32) -> binder::Result<ParcelFileDescriptor> {
         if !matches!(&*self.instance.vm_state.lock().unwrap(), VmState::Running { .. }) {
-            return Err(Status::new_service_specific_error_str(-1, Some("VM is not running")));
+            return Err(anyhow!("VM is not running")).or_service_specific_exception(-1);
         }
         let port = port as u32;
         if port < 1024 {
-            return Err(Status::new_service_specific_error_str(
-                -1,
-                Some(format!("Can't connect to privileged port {port}")),
-            ));
+            return Err(anyhow!("Can't connect to privileged port {port}"))
+                .or_service_specific_exception(-1);
         }
-        let stream = VsockStream::connect_with_cid_port(self.instance.cid, port).map_err(|e| {
-            Status::new_service_specific_error_str(-1, Some(format!("Failed to connect: {:?}", e)))
-        })?;
+        let stream = VsockStream::connect_with_cid_port(self.instance.cid, port)
+            .context("Failed to connect")
+            .or_service_specific_exception(-1)?;
         Ok(vsock_stream_to_pfd(stream))
     }
 }
@@ -1051,17 +1010,15 @@
 }
 
 /// Converts a `&ParcelFileDescriptor` to a `File` by cloning the file.
-pub fn clone_file(file: &ParcelFileDescriptor) -> Result<File, Status> {
-    file.as_ref().try_clone().map_err(|e| {
-        Status::new_exception_str(
-            ExceptionCode::BAD_PARCELABLE,
-            Some(format!("Failed to clone File from ParcelFileDescriptor: {:?}", e)),
-        )
-    })
+pub fn clone_file(file: &ParcelFileDescriptor) -> binder::Result<File> {
+    file.as_ref()
+        .try_clone()
+        .context("Failed to clone File from ParcelFileDescriptor")
+        .or_binder_exception(ExceptionCode::BAD_PARCELABLE)
 }
 
 /// Converts an `&Option<ParcelFileDescriptor>` to an `Option<File>` by cloning the file.
-fn maybe_clone_file(file: &Option<ParcelFileDescriptor>) -> Result<Option<File>, Status> {
+fn maybe_clone_file(file: &Option<ParcelFileDescriptor>) -> binder::Result<Option<File>> {
     file.as_ref().map(clone_file).transpose()
 }
 
@@ -1073,13 +1030,10 @@
 }
 
 /// Parses the platform version requirement string.
-fn parse_platform_version_req(s: &str) -> Result<VersionReq, Status> {
-    VersionReq::parse(s).map_err(|e| {
-        Status::new_exception_str(
-            ExceptionCode::BAD_PARCELABLE,
-            Some(format!("Invalid platform version requirement {}: {:?}", s, e)),
-        )
-    })
+fn parse_platform_version_req(s: &str) -> binder::Result<VersionReq> {
+    VersionReq::parse(s)
+        .with_context(|| format!("Invalid platform version requirement {}", s))
+        .or_binder_exception(ExceptionCode::BAD_PARCELABLE)
 }
 
 /// Create the empty ramdump file
@@ -1088,13 +1042,10 @@
     // VM will emit ramdump to. `ramdump_read` will be sent back to the client (i.e. the VM
     // owner) for readout.
     let ramdump_path = temporary_directory.join("ramdump");
-    let ramdump = File::create(ramdump_path).map_err(|e| {
-        error!("Failed to prepare ramdump file: {:?}", e);
-        Status::new_service_specific_error_str(
-            -1,
-            Some(format!("Failed to prepare ramdump file: {:?}", e)),
-        )
-    })?;
+    let ramdump = File::create(ramdump_path)
+        .context("Failed to prepare ramdump file")
+        .with_log()
+        .or_service_specific_exception(-1)?;
     Ok(ramdump)
 }
 
@@ -1107,20 +1058,16 @@
 
 fn check_gdb_allowed(config: &VirtualMachineConfig) -> binder::Result<()> {
     if is_protected(config) {
-        return Err(Status::new_exception_str(
-            ExceptionCode::SECURITY,
-            Some("can't use gdb with protected VMs"),
-        ));
+        return Err(anyhow!("Can't use gdb with protected VMs"))
+            .or_binder_exception(ExceptionCode::SECURITY);
     }
 
     match config {
         VirtualMachineConfig::RawConfig(_) => Ok(()),
         VirtualMachineConfig::AppConfig(config) => {
             if config.debugLevel != DebugLevel::FULL {
-                Err(Status::new_exception_str(
-                    ExceptionCode::SECURITY,
-                    Some("can't use gdb with non-debuggable VMs"),
-                ))
+                Err(anyhow!("Can't use gdb with non-debuggable VMs"))
+                    .or_binder_exception(ExceptionCode::SECURITY)
             } else {
                 Ok(())
             }
@@ -1150,9 +1097,8 @@
         return Ok(None);
     };
 
-    let (raw_read_fd, raw_write_fd) = pipe().map_err(|e| {
-        Status::new_service_specific_error_str(-1, Some(format!("Failed to create pipe: {:?}", e)))
-    })?;
+    let (raw_read_fd, raw_write_fd) =
+        pipe().context("Failed to create pipe").or_service_specific_exception(-1)?;
 
     // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
     let mut reader = BufReader::new(unsafe { File::from_raw_fd(raw_read_fd) });
@@ -1212,9 +1158,8 @@
         let cid = self.cid;
         if let Some(vm) = self.state.lock().unwrap().get_vm(cid) {
             info!("VM with CID {} started payload", cid);
-            vm.update_payload_state(PayloadState::Started).map_err(|e| {
-                Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some(e.to_string()))
-            })?;
+            vm.update_payload_state(PayloadState::Started)
+                .or_binder_exception(ExceptionCode::ILLEGAL_STATE)?;
             vm.callbacks.notify_payload_started(cid);
 
             let vm_start_timestamp = vm.vm_metric.lock().unwrap().start_timestamp;
@@ -1222,10 +1167,7 @@
             Ok(())
         } else {
             error!("notifyPayloadStarted is called from an unknown CID {}", cid);
-            Err(Status::new_service_specific_error_str(
-                -1,
-                Some(format!("cannot find a VM with CID {}", cid)),
-            ))
+            Err(anyhow!("cannot find a VM with CID {}", cid)).or_service_specific_exception(-1)
         }
     }
 
@@ -1233,17 +1175,13 @@
         let cid = self.cid;
         if let Some(vm) = self.state.lock().unwrap().get_vm(cid) {
             info!("VM with CID {} reported payload is ready", cid);
-            vm.update_payload_state(PayloadState::Ready).map_err(|e| {
-                Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some(e.to_string()))
-            })?;
+            vm.update_payload_state(PayloadState::Ready)
+                .or_binder_exception(ExceptionCode::ILLEGAL_STATE)?;
             vm.callbacks.notify_payload_ready(cid);
             Ok(())
         } else {
             error!("notifyPayloadReady is called from an unknown CID {}", cid);
-            Err(Status::new_service_specific_error_str(
-                -1,
-                Some(format!("cannot find a VM with CID {}", cid)),
-            ))
+            Err(anyhow!("cannot find a VM with CID {}", cid)).or_service_specific_exception(-1)
         }
     }
 
@@ -1251,17 +1189,13 @@
         let cid = self.cid;
         if let Some(vm) = self.state.lock().unwrap().get_vm(cid) {
             info!("VM with CID {} finished payload", cid);
-            vm.update_payload_state(PayloadState::Finished).map_err(|e| {
-                Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some(e.to_string()))
-            })?;
+            vm.update_payload_state(PayloadState::Finished)
+                .or_binder_exception(ExceptionCode::ILLEGAL_STATE)?;
             vm.callbacks.notify_payload_finished(cid, exit_code);
             Ok(())
         } else {
             error!("notifyPayloadFinished is called from an unknown CID {}", cid);
-            Err(Status::new_service_specific_error_str(
-                -1,
-                Some(format!("cannot find a VM with CID {}", cid)),
-            ))
+            Err(anyhow!("cannot find a VM with CID {}", cid)).or_service_specific_exception(-1)
         }
     }
 
@@ -1269,17 +1203,13 @@
         let cid = self.cid;
         if let Some(vm) = self.state.lock().unwrap().get_vm(cid) {
             info!("VM with CID {} encountered an error", cid);
-            vm.update_payload_state(PayloadState::Finished).map_err(|e| {
-                Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some(e.to_string()))
-            })?;
+            vm.update_payload_state(PayloadState::Finished)
+                .or_binder_exception(ExceptionCode::ILLEGAL_STATE)?;
             vm.callbacks.notify_error(cid, error_code, message);
             Ok(())
         } else {
             error!("notifyError is called from an unknown CID {}", cid);
-            Err(Status::new_service_specific_error_str(
-                -1,
-                Some(format!("cannot find a VM with CID {}", cid)),
-            ))
+            Err(anyhow!("cannot find a VM with CID {}", cid)).or_service_specific_exception(-1)
         }
     }
 
@@ -1287,10 +1217,8 @@
         let cid = self.cid;
         let Some(vm) = self.state.lock().unwrap().get_vm(cid) else {
             error!("requestCertificate is called from an unknown CID {cid}");
-            return Err(Status::new_service_specific_error_str(
-                -1,
-                Some(format!("cannot find a VM with CID {}", cid)),
-            ));
+            return Err(anyhow!("cannot find a VM with CID {}", cid))
+                .or_service_specific_exception(-1);
         };
         let instance_img_path = vm.temporary_directory.join("rkpvm_instance.img");
         let instance_img = OpenOptions::new()
@@ -1298,13 +1226,9 @@
             .read(true)
             .write(true)
             .open(instance_img_path)
-            .map_err(|e| {
-                error!("Failed to create rkpvm_instance.img file: {:?}", e);
-                Status::new_service_specific_error_str(
-                    -1,
-                    Some(format!("Failed to create rkpvm_instance.img file: {:?}", e)),
-                )
-            })?;
+            .context("Failed to create rkpvm_instance.img file")
+            .with_log()
+            .or_service_specific_exception(-1)?;
         GLOBAL_SERVICE.requestCertificate(csr, &ParcelFileDescriptor::new(instance_img))
     }
 }
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 68cc7f2..6372fa8 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -529,8 +529,10 @@
                         MemoryTrimLevel::TRIM_MEMORY_RUNNING_MODERATE => 10,
                         _ => bail!("Invalid memory trim level {:?}", level),
                     };
-                    let command =
-                        BalloonControlCommand::Adjust { num_bytes: total_memory * pct / 100 };
+                    let command = BalloonControlCommand::Adjust {
+                        num_bytes: total_memory * pct / 100,
+                        wait_for_success: false,
+                    };
                     if let Err(e) = vm_control::client::handle_request(
                         &VmRequest::BalloonCommand(command),
                         &self.crosvm_control_socket_path,
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 6b39ff9..67890e2 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -27,12 +27,13 @@
         "android.os.permissions_aidl-rust",
         "libandroid_logger",
         "libanyhow",
+        "libavflog",
         "libbinder_rs",
-        "libvmclient",
         "liblibc",
         "liblog_rust",
         "libnix",
         "librustutils",
+        "libvmclient",
         "libstatslog_virtualization_rust",
         "libtombstoned_client_rust",
         "libvsock",
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 2e667d4..b2513d9 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -33,7 +33,8 @@
 };
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::VM_TOMBSTONES_SERVICE_PORT;
 use anyhow::{anyhow, ensure, Context, Result};
-use binder::{self, wait_for_interface, BinderFeatures, ExceptionCode, Interface, LazyServiceGuard, Status, Strong};
+use avflog::LogResult;
+use binder::{self, wait_for_interface, BinderFeatures, ExceptionCode, Interface, LazyServiceGuard, Status, Strong, IntoBinderResult};
 use libc::VMADDR_CID_HOST;
 use log::{error, info, warn};
 use rustutils::system_properties;
@@ -42,7 +43,7 @@
 use std::io::{Read, Write};
 use std::os::unix::fs::PermissionsExt;
 use std::os::unix::raw::{pid_t, uid_t};
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 use std::sync::{Arc, Mutex, Weak};
 use tombstoned_client::{DebuggerdDumpType, TombstonedConnection};
 use vsock::{VsockListener, VsockStream};
@@ -102,15 +103,10 @@
 
         match ret {
             0 => Ok(()),
-            -1 => Err(Status::new_exception_str(
-                ExceptionCode::ILLEGAL_STATE,
-                Some(std::io::Error::last_os_error().to_string()),
-            )),
-            n => Err(Status::new_exception_str(
-                ExceptionCode::ILLEGAL_STATE,
-                Some(format!("Unexpected return value from prlimit(): {n}")),
-            )),
+            -1 => Err(std::io::Error::last_os_error().into()),
+            n => Err(anyhow!("Unexpected return value from prlimit(): {n}")),
         }
+        .or_binder_exception(ExceptionCode::ILLEGAL_STATE)
     }
 
     fn allocateGlobalVmContext(
@@ -122,9 +118,9 @@
         let requester_uid = get_calling_uid();
         let requester_debug_pid = requester_debug_pid as pid_t;
         let state = &mut *self.state.lock().unwrap();
-        state.allocate_vm_context(requester_uid, requester_debug_pid).map_err(|e| {
-            Status::new_exception_str(ExceptionCode::ILLEGAL_STATE, Some(e.to_string()))
-        })
+        state
+            .allocate_vm_context(requester_uid, requester_debug_pid)
+            .or_binder_exception(ExceptionCode::ILLEGAL_STATE)
     }
 
     fn atomVmBooted(&self, atom: &AtomVmBooted) -> Result<(), Status> {
@@ -167,20 +163,22 @@
     ) -> binder::Result<Vec<u8>> {
         check_manage_access()?;
         info!("Received csr. Getting certificate...");
-        request_certificate(csr, instance_img_fd).map_err(|e| {
-            error!("Failed to get certificate. Error: {e:?}");
-            Status::new_exception_str(ExceptionCode::SERVICE_SPECIFIC, Some(e.to_string()))
-        })
+        request_certificate(csr, instance_img_fd)
+            .context("Failed to get certificate")
+            .with_log()
+            .or_service_specific_exception(-1)
     }
 
     fn getAssignableDevices(&self) -> binder::Result<Vec<AssignableDevice>> {
         check_use_custom_virtual_machine()?;
 
         // TODO(b/291191362): read VM DTBO to find assignable devices.
-        Ok(vec![AssignableDevice {
-            kind: "eh".to_owned(),
-            node: "/sys/bus/platform/devices/16d00000.eh".to_owned(),
-        }])
+        let mut devices = Vec::new();
+        let eh_path = "/sys/bus/platform/devices/16d00000.eh";
+        if Path::new(eh_path).exists() {
+            devices.push(AssignableDevice { kind: "eh".to_owned(), node: eh_path.to_owned() });
+        }
+        Ok(devices)
     }
 
     fn bindDevicesToVfioDriver(
@@ -403,10 +401,8 @@
     if perm_svc.checkPermission(perm, calling_pid, calling_uid as i32)? {
         Ok(())
     } else {
-        Err(Status::new_exception_str(
-            ExceptionCode::SECURITY,
-            Some(format!("does not have the {} permission", perm)),
-        ))
+        Err(anyhow!("does not have the {} permission", perm))
+            .or_binder_exception(ExceptionCode::SECURITY)
     }
 }
 
diff --git a/virtualizationservice/vfio_handler/Android.bp b/virtualizationservice/vfio_handler/Android.bp
index efbb7b5..66662d5 100644
--- a/virtualizationservice/vfio_handler/Android.bp
+++ b/virtualizationservice/vfio_handler/Android.bp
@@ -22,10 +22,13 @@
     rustlibs: [
         "android.system.virtualizationservice_internal-rust",
         "libandroid_logger",
+        "libanyhow",
         "libbinder_rs",
+        "liblazy_static",
         "liblog_rust",
         "libnix",
-        "liblazy_static",
+        "librustutils",
+        "libzerocopy",
     ],
     apex_available: ["com.android.virt"],
 }
diff --git a/virtualizationservice/vfio_handler/src/aidl.rs b/virtualizationservice/vfio_handler/src/aidl.rs
index f082aba..bb9faf1 100644
--- a/virtualizationservice/vfio_handler/src/aidl.rs
+++ b/virtualizationservice/vfio_handler/src/aidl.rs
@@ -14,13 +14,20 @@
 
 //! Implementation of the AIDL interface of the VirtualizationService.
 
+use anyhow::{anyhow, Context};
 use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IVfioHandler::IVfioHandler;
 use android_system_virtualizationservice_internal::binder::ParcelFileDescriptor;
-use binder::{self, ExceptionCode, Interface, Status};
+use binder::{self, ExceptionCode, Interface, IntoBinderResult};
 use lazy_static::lazy_static;
-use std::fs::{read_link, write};
-use std::io::Write;
-use std::path::Path;
+use std::fs::{read_link, write, File};
+use std::io::{Read, Seek, SeekFrom, Write};
+use std::mem::size_of;
+use std::path::{Path, PathBuf};
+use rustutils::system_properties;
+use zerocopy::{
+    byteorder::{BigEndian, U32},
+    FromBytes,
+};
 
 #[derive(Debug, Default)]
 pub struct VfioHandler {}
@@ -41,27 +48,13 @@
     ) -> binder::Result<()> {
         // permission check is already done by IVirtualizationServiceInternal.
         if !*IS_VFIO_SUPPORTED {
-            return Err(Status::new_exception_str(
-                ExceptionCode::UNSUPPORTED_OPERATION,
-                Some("VFIO-platform not supported"),
-            ));
+            return Err(anyhow!("VFIO-platform not supported"))
+                .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION);
         }
-
         devices.iter().try_for_each(|x| bind_device(Path::new(x)))?;
 
-        let mut dtbo = dtbo.as_ref().try_clone().map_err(|e| {
-            Status::new_exception_str(
-                ExceptionCode::BAD_PARCELABLE,
-                Some(format!("Failed to clone File from ParcelFileDescriptor: {e:?}")),
-            )
-        })?;
-        // TODO(b/291191362): write DTBO for devices to dtbo.
-        dtbo.write(b"\n").map_err(|e| {
-            Status::new_exception_str(
-                ExceptionCode::BAD_PARCELABLE,
-                Some(format!("Can't write to ParcelFileDescriptor: {e:?}")),
-            )
-        })?;
+        write_dtbo(dtbo)?;
+
         Ok(())
     }
 }
@@ -70,28 +63,63 @@
 const SYSFS_PLATFORM_DEVICES_PATH: &str = "/sys/devices/platform/";
 const VFIO_PLATFORM_DRIVER_PATH: &str = "/sys/bus/platform/drivers/vfio-platform";
 const SYSFS_PLATFORM_DRIVERS_PROBE_PATH: &str = "/sys/bus/platform/drivers_probe";
+const DT_TABLE_MAGIC: u32 = 0xd7b7ab1e;
 
-lazy_static! {
-    static ref IS_VFIO_SUPPORTED: bool = is_vfio_supported();
+/// The structure of DT table header in dtbo.img.
+/// https://source.android.com/docs/core/architecture/dto/partitions
+#[repr(C)]
+#[derive(Debug, FromBytes)]
+struct DtTableHeader {
+    /// DT_TABLE_MAGIC
+    magic: U32<BigEndian>,
+    /// includes dt_table_header + all dt_table_entry and all dtb/dtbo
+    _total_size: U32<BigEndian>,
+    /// sizeof(dt_table_header)
+    header_size: U32<BigEndian>,
+    /// sizeof(dt_table_entry)
+    dt_entry_size: U32<BigEndian>,
+    /// number of dt_table_entry
+    dt_entry_count: U32<BigEndian>,
+    /// offset to the first dt_table_entry from head of dt_table_header
+    dt_entries_offset: U32<BigEndian>,
+    /// flash page size we assume
+    _page_size: U32<BigEndian>,
+    /// DTBO image version, the current version is 0. The version will be
+    /// incremented when the dt_table_header struct is updated.
+    _version: U32<BigEndian>,
 }
 
-fn is_vfio_supported() -> bool {
-    Path::new(DEV_VFIO_PATH).exists() && Path::new(VFIO_PLATFORM_DRIVER_PATH).exists()
+/// The structure of each DT table entry (v0) in dtbo.img.
+/// https://source.android.com/docs/core/architecture/dto/partitions
+#[repr(C)]
+#[derive(Debug, FromBytes)]
+struct DtTableEntry {
+    /// size of each DT
+    dt_size: U32<BigEndian>,
+    /// offset from head of dt_table_header
+    dt_offset: U32<BigEndian>,
+    /// optional, must be zero if unused
+    _id: U32<BigEndian>,
+    /// optional, must be zero if unused
+    _rev: U32<BigEndian>,
+    /// optional, must be zero if unused
+    _custom: [U32<BigEndian>; 4],
+}
+
+lazy_static! {
+    static ref IS_VFIO_SUPPORTED: bool =
+        Path::new(DEV_VFIO_PATH).exists() && Path::new(VFIO_PLATFORM_DRIVER_PATH).exists();
 }
 
 fn check_platform_device(path: &Path) -> binder::Result<()> {
     if !path.exists() {
-        return Err(Status::new_exception_str(
-            ExceptionCode::ILLEGAL_ARGUMENT,
-            Some(format!("no such device {path:?}")),
-        ));
+        return Err(anyhow!("no such device {path:?}"))
+            .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT);
     }
 
     if !path.starts_with(SYSFS_PLATFORM_DEVICES_PATH) {
-        return Err(Status::new_exception_str(
-            ExceptionCode::ILLEGAL_ARGUMENT,
-            Some(format!("{path:?} is not a platform device")),
-        ));
+        return Err(anyhow!("{path:?} is not a platform device"))
+            .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT);
     }
 
     Ok(())
@@ -121,65 +149,155 @@
 
     // unbind
     let Some(device) = path.file_name() else {
-        return Err(Status::new_exception_str(
-            ExceptionCode::ILLEGAL_ARGUMENT,
-            Some(format!("can't get device name from {path:?}")),
-        ));
+        return Err(anyhow!("can't get device name from {path:?}"))
+            .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT);
     };
     let Some(device_str) = device.to_str() else {
-        return Err(Status::new_exception_str(
-            ExceptionCode::ILLEGAL_ARGUMENT,
-            Some(format!("invalid filename {device:?}")),
-        ));
+        return Err(anyhow!("invalid filename {device:?}"))
+            .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT);
     };
-    write(path.join("driver/unbind"), device_str.as_bytes()).map_err(|e| {
-        Status::new_exception_str(
-            ExceptionCode::SERVICE_SPECIFIC,
-            Some(format!("could not unbind {device_str}: {e:?}")),
-        )
-    })?;
+    let unbind_path = path.join("driver/unbind");
+    if unbind_path.exists() {
+        write(&unbind_path, device_str.as_bytes())
+            .with_context(|| format!("could not unbind {device_str}"))
+            .or_service_specific_exception(-1)?;
+    }
 
     // bind to VFIO
-    write(path.join("driver_override"), b"vfio-platform").map_err(|e| {
-        Status::new_exception_str(
-            ExceptionCode::SERVICE_SPECIFIC,
-            Some(format!("could not bind {device_str} to vfio-platform: {e:?}")),
-        )
-    })?;
+    write(path.join("driver_override"), b"vfio-platform")
+        .with_context(|| format!("could not bind {device_str} to vfio-platform"))
+        .or_service_specific_exception(-1)?;
 
-    write(SYSFS_PLATFORM_DRIVERS_PROBE_PATH, device_str.as_bytes()).map_err(|e| {
-        Status::new_exception_str(
-            ExceptionCode::SERVICE_SPECIFIC,
-            Some(format!("could not write {device_str} to drivers-probe: {e:?}")),
-        )
-    })?;
+    write(SYSFS_PLATFORM_DRIVERS_PROBE_PATH, device_str.as_bytes())
+        .with_context(|| format!("could not write {device_str} to drivers-probe"))
+        .or_service_specific_exception(-1)?;
 
     // final check
     if !is_bound_to_vfio_driver(path) {
-        return Err(Status::new_exception_str(
-            ExceptionCode::SERVICE_SPECIFIC,
-            Some(format!("{path:?} still not bound to vfio driver")),
-        ));
+        return Err(anyhow!("{path:?} still not bound to vfio driver"))
+            .or_service_specific_exception(-1);
     }
 
     if get_device_iommu_group(path).is_none() {
-        return Err(Status::new_exception_str(
-            ExceptionCode::SERVICE_SPECIFIC,
-            Some(format!("can't get iommu group for {path:?}")),
-        ));
+        return Err(anyhow!("can't get iommu group for {path:?}"))
+            .or_service_specific_exception(-1);
     }
 
     Ok(())
 }
 
 fn bind_device(path: &Path) -> binder::Result<()> {
-    let path = path.canonicalize().map_err(|e| {
-        Status::new_exception_str(
-            ExceptionCode::ILLEGAL_ARGUMENT,
-            Some(format!("can't canonicalize {path:?}: {e:?}")),
-        )
-    })?;
+    let path = path
+        .canonicalize()
+        .with_context(|| format!("can't canonicalize {path:?}"))
+        .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT)?;
 
     check_platform_device(&path)?;
     bind_vfio_driver(&path)
 }
+
+fn get_dtbo_img_path() -> binder::Result<PathBuf> {
+    let slot_suffix = system_properties::read("ro.boot.slot_suffix")
+        .context("Failed to read ro.boot.slot_suffix")
+        .or_service_specific_exception(-1)?
+        .ok_or_else(|| anyhow!("slot_suffix is none"))
+        .or_service_specific_exception(-1)?;
+    Ok(PathBuf::from(format!("/dev/block/by-name/dtbo{slot_suffix}")))
+}
+
+fn read_values(file: &mut File, size: usize, offset: u64) -> binder::Result<Vec<u8>> {
+    file.seek(SeekFrom::Start(offset))
+        .context("Cannot seek the offset")
+        .or_service_specific_exception(-1)?;
+    let mut buffer = vec![0_u8; size];
+    file.read_exact(&mut buffer)
+        .context("Failed to read buffer")
+        .or_service_specific_exception(-1)?;
+    Ok(buffer)
+}
+
+fn get_dt_table_header(file: &mut File) -> binder::Result<DtTableHeader> {
+    let values = read_values(file, size_of::<DtTableHeader>(), 0)?;
+    let dt_table_header = DtTableHeader::read_from(values.as_slice())
+        .context("DtTableHeader is invalid")
+        .or_service_specific_exception(-1)?;
+    if dt_table_header.magic.get() != DT_TABLE_MAGIC
+        || dt_table_header.header_size.get() as usize != size_of::<DtTableHeader>()
+    {
+        return Err(anyhow!("DtTableHeader is invalid")).or_service_specific_exception(-1)?;
+    }
+    Ok(dt_table_header)
+}
+
+fn get_dt_table_entry(
+    file: &mut File,
+    header: &DtTableHeader,
+    index: u32,
+) -> binder::Result<DtTableEntry> {
+    if index >= header.dt_entry_count.get() {
+        return Err(anyhow!("Invalid dtbo index {index}")).or_service_specific_exception(-1)?;
+    }
+    let Some(prev_dt_entry_total_size) = header.dt_entry_size.get().checked_mul(index) else {
+        return Err(anyhow!("Unexpected arithmetic result"))
+            .or_binder_exception(ExceptionCode::ILLEGAL_STATE);
+    };
+    let Some(dt_entry_offset) =
+        prev_dt_entry_total_size.checked_add(header.dt_entries_offset.get())
+    else {
+        return Err(anyhow!("Unexpected arithmetic result"))
+            .or_binder_exception(ExceptionCode::ILLEGAL_STATE);
+    };
+    let values = read_values(file, size_of::<DtTableEntry>(), dt_entry_offset.into())?;
+    let dt_table_entry = DtTableEntry::read_from(values.as_slice())
+        .with_context(|| format!("DtTableEntry at index {index} is invalid."))
+        .or_service_specific_exception(-1)?;
+    Ok(dt_table_entry)
+}
+
+fn filter_dtbo_from_img(
+    dtbo_img_file: &mut File,
+    entry: &DtTableEntry,
+    dtbo_fd: &ParcelFileDescriptor,
+) -> binder::Result<()> {
+    let dt_size = entry
+        .dt_size
+        .get()
+        .try_into()
+        .context("Failed to convert type")
+        .or_binder_exception(ExceptionCode::ILLEGAL_STATE)?;
+    let buffer = read_values(dtbo_img_file, dt_size, entry.dt_offset.get().into())?;
+
+    let mut dtbo_fd = dtbo_fd
+        .as_ref()
+        .try_clone()
+        .context("Failed to clone File from ParcelFileDescriptor")
+        .or_binder_exception(ExceptionCode::BAD_PARCELABLE)?;
+
+    // TODO(b/296796644): Filter dtbo.img, not writing all information.
+    dtbo_fd
+        .write_all(&buffer)
+        .context("Failed to write dtbo file")
+        .or_service_specific_exception(-1)?;
+    Ok(())
+}
+
+fn write_dtbo(dtbo_fd: &ParcelFileDescriptor) -> binder::Result<()> {
+    let dtbo_path = get_dtbo_img_path()?;
+    let mut dtbo_img = File::open(dtbo_path)
+        .context("Failed to open DTBO partition")
+        .or_service_specific_exception(-1)?;
+
+    let dt_table_header = get_dt_table_header(&mut dtbo_img)?;
+    let vm_dtbo_idx = system_properties::read("ro.boot.hypervisor.vm_dtbo_idx")
+        .context("Failed to read vm_dtbo_idx")
+        .or_service_specific_exception(-1)?
+        .ok_or_else(|| anyhow!("vm_dtbo_idx is none"))
+        .or_service_specific_exception(-1)?;
+    let vm_dtbo_idx = vm_dtbo_idx
+        .parse()
+        .context("vm_dtbo_idx is not an integer")
+        .or_service_specific_exception(-1)?;
+    let dt_table_entry = get_dt_table_entry(&mut dtbo_img, &dt_table_header, vm_dtbo_idx)?;
+    filter_dtbo_from_img(&mut dtbo_img, &dt_table_entry, dtbo_fd)?;
+    Ok(())
+}
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 64bcd02..4c44496 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -24,7 +24,7 @@
 };
 use anyhow::{Context, Error};
 use binder::{ProcessState, Strong};
-use clap::Parser;
+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};
@@ -34,201 +34,165 @@
 #[derive(Debug)]
 struct Idsigs(Vec<PathBuf>);
 
+#[derive(Args)]
+/// Collection of flags that are at VM level and therefore applicable to all subcommands
+pub struct CommonConfig {
+    /// Name of VM
+    #[arg(long)]
+    name: Option<String>,
+
+    /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
+    #[arg(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
+    cpu_topology: CpuTopology,
+
+    /// Comma separated list of task profile names to apply to the VM
+    #[arg(long)]
+    task_profiles: Vec<String>,
+
+    /// Memory size (in MiB) of the VM. If unspecified, defaults to the value of `memory_mib`
+    /// in the VM config file.
+    #[arg(short, long)]
+    mem: Option<u32>,
+
+    /// Run VM in protected mode.
+    #[arg(short, long)]
+    protected: bool,
+}
+
+#[derive(Args)]
+/// Collection of flags for debugging
+pub struct DebugConfig {
+    /// Debug level of the VM. Supported values: "full" (default), and "none".
+    #[arg(long, default_value = "full", value_parser = parse_debug_level)]
+    debug: DebugLevel,
+
+    /// Path to file for VM console output.
+    #[arg(long)]
+    console: Option<PathBuf>,
+
+    /// Path to file for VM console input.
+    #[arg(long)]
+    console_in: Option<PathBuf>,
+
+    /// Path to file for VM log output.
+    #[arg(long)]
+    log: Option<PathBuf>,
+
+    /// Port at which crosvm will start a gdb server to debug guest kernel.
+    /// Note: this is only supported on Android kernels android14-5.15 and higher.
+    #[arg(long)]
+    gdb: Option<NonZeroU16>,
+}
+
+#[derive(Args)]
+/// Collection of flags that are Microdroid specific
+pub struct MicrodroidConfig {
+    /// Path to the file backing the storage.
+    /// Created if the option is used but the path does not exist in the device.
+    #[arg(long)]
+    storage: Option<PathBuf>,
+
+    /// Size of the storage. Used only if --storage is supplied but path does not exist
+    /// Default size is 10*1024*1024
+    #[arg(long)]
+    storage_size: Option<u64>,
+
+    /// Path to custom kernel image to use when booting Microdroid.
+    #[arg(long)]
+    kernel: Option<PathBuf>,
+
+    /// Path to disk image containing vendor-specific modules.
+    #[arg(long)]
+    vendor: Option<PathBuf>,
+
+    /// SysFS nodes of devices to assign to VM
+    #[arg(long)]
+    devices: Vec<PathBuf>,
+}
+
+#[derive(Args)]
+/// Flags for the run_app subcommand
+pub struct RunAppConfig {
+    #[command(flatten)]
+    common: CommonConfig,
+
+    #[command(flatten)]
+    debug: DebugConfig,
+
+    #[command(flatten)]
+    microdroid: MicrodroidConfig,
+
+    /// Path to VM Payload APK
+    apk: PathBuf,
+
+    /// Path to idsig of the APK
+    idsig: PathBuf,
+
+    /// Path to the instance image. Created if not exists.
+    instance: PathBuf,
+
+    /// Path to VM config JSON within APK (e.g. assets/vm_config.json)
+    #[arg(long)]
+    config_path: Option<String>,
+
+    /// Name of VM payload binary within APK (e.g. MicrodroidTestNativeLib.so)
+    #[arg(long)]
+    #[arg(alias = "payload_path")]
+    payload_binary_name: Option<String>,
+
+    /// Paths to extra idsig files.
+    #[arg(long = "extra-idsig")]
+    extra_idsigs: Vec<PathBuf>,
+}
+
+#[derive(Args)]
+/// Flags for the run_microdroid subcommand
+pub struct RunMicrodroidConfig {
+    #[command(flatten)]
+    common: CommonConfig,
+
+    #[command(flatten)]
+    debug: DebugConfig,
+
+    #[command(flatten)]
+    microdroid: MicrodroidConfig,
+
+    /// Path to the directory where VM-related files (e.g. instance.img, apk.idsig, etc.) will
+    /// be stored. If not specified a random directory under /data/local/tmp/microdroid will be
+    /// created and used.
+    #[arg(long)]
+    work_dir: Option<PathBuf>,
+}
+
+#[derive(Args)]
+/// Flags for the run subcommand
+pub struct RunCustomVmConfig {
+    #[command(flatten)]
+    common: CommonConfig,
+
+    #[command(flatten)]
+    debug: DebugConfig,
+
+    /// Path to VM config JSON
+    config: PathBuf,
+}
+
 #[derive(Parser)]
 enum Opt {
     /// Run a virtual machine with a config in APK
     RunApp {
-        /// Path to VM Payload APK
-        apk: PathBuf,
-
-        /// Path to idsig of the APK
-        idsig: PathBuf,
-
-        /// Path to the instance image. Created if not exists.
-        instance: PathBuf,
-
-        /// Path to VM config JSON within APK (e.g. assets/vm_config.json)
-        #[clap(long)]
-        config_path: Option<String>,
-
-        /// Name of VM payload binary within APK (e.g. MicrodroidTestNativeLib.so)
-        #[clap(long)]
-        #[clap(alias = "payload_path")]
-        payload_binary_name: Option<String>,
-
-        /// Name of VM
-        #[clap(long)]
-        name: Option<String>,
-
-        /// Path to the file backing the storage.
-        /// Created if the option is used but the path does not exist in the device.
-        #[clap(long)]
-        storage: Option<PathBuf>,
-
-        /// Size of the storage. Used only if --storage is supplied but path does not exist
-        /// Default size is 10*1024*1024
-        #[clap(long)]
-        storage_size: Option<u64>,
-
-        /// Path to file for VM console output.
-        #[clap(long)]
-        console: Option<PathBuf>,
-
-        /// Path to file for VM console input.
-        #[clap(long)]
-        console_in: Option<PathBuf>,
-
-        /// Path to file for VM log output.
-        #[clap(long)]
-        log: Option<PathBuf>,
-
-        /// Debug level of the VM. Supported values: "full" (default), and "none".
-        #[clap(long, default_value = "full", value_parser = parse_debug_level)]
-        debug: DebugLevel,
-
-        /// Run VM in protected mode.
-        #[clap(short, long)]
-        protected: bool,
-
-        /// Memory size (in MiB) of the VM. If unspecified, defaults to the value of `memory_mib`
-        /// in the VM config file.
-        #[clap(short, long)]
-        mem: Option<u32>,
-
-        /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
-        #[clap(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
-        cpu_topology: CpuTopology,
-
-        /// Comma separated list of task profile names to apply to the VM
-        #[clap(long)]
-        task_profiles: Vec<String>,
-
-        /// Paths to extra idsig files.
-        #[clap(long = "extra-idsig")]
-        extra_idsigs: Vec<PathBuf>,
-
-        /// Port at which crosvm will start a gdb server to debug guest kernel.
-        /// Note: this is only supported on Android kernels android14-5.15 and higher.
-        #[clap(long)]
-        gdb: Option<NonZeroU16>,
-
-        /// Path to custom kernel image to use when booting Microdroid.
-        #[clap(long)]
-        kernel: Option<PathBuf>,
-
-        /// Path to disk image containing vendor-specific modules.
-        #[clap(long)]
-        vendor: Option<PathBuf>,
-
-        /// SysFS nodes of devices to assign to VM
-        #[clap(long)]
-        devices: Vec<PathBuf>,
+        #[command(flatten)]
+        config: RunAppConfig,
     },
     /// Run a virtual machine with Microdroid inside
     RunMicrodroid {
-        /// Path to the directory where VM-related files (e.g. instance.img, apk.idsig, etc.) will
-        /// be stored. If not specified a random directory under /data/local/tmp/microdroid will be
-        /// created and used.
-        #[clap(long)]
-        work_dir: Option<PathBuf>,
-
-        /// Name of VM
-        #[clap(long)]
-        name: Option<String>,
-
-        /// Path to the file backing the storage.
-        /// Created if the option is used but the path does not exist in the device.
-        #[clap(long)]
-        storage: Option<PathBuf>,
-
-        /// Size of the storage. Used only if --storage is supplied but path does not exist
-        /// Default size is 10*1024*1024
-        #[clap(long)]
-        storage_size: Option<u64>,
-
-        /// Path to file for VM console output.
-        #[clap(long)]
-        console: Option<PathBuf>,
-
-        /// Path to file for VM console input.
-        #[clap(long)]
-        console_in: Option<PathBuf>,
-
-        /// Path to file for VM log output.
-        #[clap(long)]
-        log: Option<PathBuf>,
-
-        /// Debug level of the VM. Supported values: "full" (default), and "none".
-        #[clap(long, default_value = "full", value_parser = parse_debug_level)]
-        debug: DebugLevel,
-
-        /// Run VM in protected mode.
-        #[clap(short, long)]
-        protected: bool,
-
-        /// Memory size (in MiB) of the VM. If unspecified, defaults to the value of `memory_mib`
-        /// in the VM config file.
-        #[clap(short, long)]
-        mem: Option<u32>,
-
-        /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
-        #[clap(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
-        cpu_topology: CpuTopology,
-
-        /// Comma separated list of task profile names to apply to the VM
-        #[clap(long)]
-        task_profiles: Vec<String>,
-
-        /// Port at which crosvm will start a gdb server to debug guest kernel.
-        /// Note: this is only supported on Android kernels android14-5.15 and higher.
-        #[clap(long)]
-        gdb: Option<NonZeroU16>,
-
-        /// Path to custom kernel image to use when booting Microdroid.
-        #[clap(long)]
-        kernel: Option<PathBuf>,
-
-        /// Path to disk image containing vendor-specific modules.
-        #[clap(long)]
-        vendor: Option<PathBuf>,
-
-        /// SysFS nodes of devices to assign to VM
-        #[clap(long)]
-        devices: Vec<PathBuf>,
+        #[command(flatten)]
+        config: RunMicrodroidConfig,
     },
     /// Run a virtual machine
     Run {
-        /// Path to VM config JSON
-        config: PathBuf,
-
-        /// Name of VM
-        #[clap(long)]
-        name: Option<String>,
-
-        /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
-        #[clap(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
-        cpu_topology: CpuTopology,
-
-        /// Comma separated list of task profile names to apply to the VM
-        #[clap(long)]
-        task_profiles: Vec<String>,
-
-        /// Path to file for VM console output.
-        #[clap(long)]
-        console: Option<PathBuf>,
-
-        /// Path to file for VM console input.
-        #[clap(long)]
-        console_in: Option<PathBuf>,
-
-        /// Path to file for VM log output.
-        #[clap(long)]
-        log: Option<PathBuf>,
-
-        /// Port at which crosvm will start a gdb server to debug guest kernel.
-        /// Note: this is only supported on Android kernels android14-5.15 and higher.
-        #[clap(long)]
-        gdb: Option<NonZeroU16>,
+        #[command(flatten)]
+        config: RunCustomVmConfig,
     },
     /// List running virtual machines
     List,
@@ -243,7 +207,7 @@
         size: u64,
 
         /// Type of the partition
-        #[clap(short = 't', long = "type", default_value = "raw",
+        #[arg(short = 't', long = "type", default_value = "raw",
                value_parser = parse_partition_type)]
         partition_type: PartitionType,
     },
@@ -295,102 +259,9 @@
     ProcessState::start_thread_pool();
 
     match opt {
-        Opt::RunApp {
-            name,
-            apk,
-            idsig,
-            instance,
-            storage,
-            storage_size,
-            config_path,
-            payload_binary_name,
-            console,
-            console_in,
-            log,
-            debug,
-            protected,
-            mem,
-            cpu_topology,
-            task_profiles,
-            extra_idsigs,
-            gdb,
-            kernel,
-            vendor,
-            devices,
-        } => command_run_app(
-            name,
-            get_service()?.as_ref(),
-            &apk,
-            &idsig,
-            &instance,
-            storage.as_deref(),
-            storage_size,
-            config_path,
-            payload_binary_name,
-            console.as_deref(),
-            console_in.as_deref(),
-            log.as_deref(),
-            debug,
-            protected,
-            mem,
-            cpu_topology,
-            task_profiles,
-            &extra_idsigs,
-            gdb,
-            kernel.as_deref(),
-            vendor.as_deref(),
-            devices,
-        ),
-        Opt::RunMicrodroid {
-            name,
-            work_dir,
-            storage,
-            storage_size,
-            console,
-            console_in,
-            log,
-            debug,
-            protected,
-            mem,
-            cpu_topology,
-            task_profiles,
-            gdb,
-            kernel,
-            vendor,
-            devices,
-        } => command_run_microdroid(
-            name,
-            get_service()?.as_ref(),
-            work_dir,
-            storage.as_deref(),
-            storage_size,
-            console.as_deref(),
-            console_in.as_deref(),
-            log.as_deref(),
-            debug,
-            protected,
-            mem,
-            cpu_topology,
-            task_profiles,
-            gdb,
-            kernel.as_deref(),
-            vendor.as_deref(),
-            devices,
-        ),
-        Opt::Run { name, config, cpu_topology, task_profiles, console, console_in, log, gdb } => {
-            command_run(
-                name,
-                get_service()?.as_ref(),
-                &config,
-                console.as_deref(),
-                console_in.as_deref(),
-                log.as_deref(),
-                /* mem */ None,
-                cpu_topology,
-                task_profiles,
-                gdb,
-            )
-        }
+        Opt::RunApp { config } => command_run_app(config),
+        Opt::RunMicrodroid { config } => command_run_microdroid(config),
+        Opt::Run { config } => command_run(config),
         Opt::List => command_list(get_service()?.as_ref()),
         Opt::Info => command_info(),
         Opt::CreatePartition { path, size, partition_type } => {
@@ -444,6 +315,10 @@
         println!("VFIO-platform is not supported.");
     }
 
+    let devices = get_service()?.getAssignableDevices()?;
+    let devices = devices.into_iter().map(|x| x.node).collect::<Vec<_>>();
+    println!("Assignable devices: {}", serde_json::to_string(&devices)?);
+
     Ok(())
 }
 
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 250c56c..fc8d7e0 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -15,8 +15,8 @@
 //! Command to run a VM.
 
 use crate::create_partition::command_create_partition;
+use crate::{get_service, RunAppConfig, RunCustomVmConfig, RunMicrodroidConfig};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
-    CpuTopology::CpuTopology,
     IVirtualizationService::IVirtualizationService,
     PartitionType::PartitionType,
     VirtualMachineAppConfig::{
@@ -35,7 +35,6 @@
 use std::fs;
 use std::fs::File;
 use std::io;
-use std::num::NonZeroU16;
 use std::os::unix::io::{AsRawFd, FromRawFd};
 use std::path::{Path, PathBuf};
 use vmclient::{ErrorCode, VmInstance};
@@ -43,98 +42,78 @@
 use zip::ZipArchive;
 
 /// Run a VM from the given APK, idsig, and config.
-#[allow(clippy::too_many_arguments)]
-pub fn command_run_app(
-    name: Option<String>,
-    service: &dyn IVirtualizationService,
-    apk: &Path,
-    idsig: &Path,
-    instance: &Path,
-    storage: Option<&Path>,
-    storage_size: Option<u64>,
-    config_path: Option<String>,
-    payload_binary_name: Option<String>,
-    console_out_path: Option<&Path>,
-    console_in_path: Option<&Path>,
-    log_path: Option<&Path>,
-    debug_level: DebugLevel,
-    protected: bool,
-    mem: Option<u32>,
-    cpu_topology: CpuTopology,
-    task_profiles: Vec<String>,
-    extra_idsigs: &[PathBuf],
-    gdb: Option<NonZeroU16>,
-    kernel: Option<&Path>,
-    vendor: Option<&Path>,
-    devices: Vec<PathBuf>,
-) -> Result<(), Error> {
-    let apk_file = File::open(apk).context("Failed to open APK file")?;
+pub fn command_run_app(config: RunAppConfig) -> Result<(), Error> {
+    let service = get_service()?;
+    let apk = File::open(&config.apk).context("Failed to open APK file")?;
 
-    let extra_apks = match config_path.as_deref() {
-        Some(path) => parse_extra_apk_list(apk, path)?,
+    let extra_apks = match config.config_path.as_deref() {
+        Some(path) => parse_extra_apk_list(&config.apk, path)?,
         None => vec![],
     };
 
-    if extra_apks.len() != extra_idsigs.len() {
+    if extra_apks.len() != config.extra_idsigs.len() {
         bail!(
             "Found {} extra apks, but there are {} extra idsigs",
             extra_apks.len(),
-            extra_idsigs.len()
+            config.extra_idsigs.len()
         )
     }
 
-    for i in 0..extra_apks.len() {
-        let extra_apk_fd = ParcelFileDescriptor::new(File::open(&extra_apks[i])?);
-        let extra_idsig_fd = ParcelFileDescriptor::new(File::create(&extra_idsigs[i])?);
+    for (i, extra_apk) in extra_apks.iter().enumerate() {
+        let extra_apk_fd = ParcelFileDescriptor::new(File::open(extra_apk)?);
+        let extra_idsig_fd = ParcelFileDescriptor::new(File::create(&config.extra_idsigs[i])?);
         service.createOrUpdateIdsigFile(&extra_apk_fd, &extra_idsig_fd)?;
     }
 
-    let idsig_file = File::create(idsig).context("Failed to create idsig file")?;
+    let idsig = File::create(&config.idsig).context("Failed to create idsig file")?;
 
-    let apk_fd = ParcelFileDescriptor::new(apk_file);
-    let idsig_fd = ParcelFileDescriptor::new(idsig_file);
+    let apk_fd = ParcelFileDescriptor::new(apk);
+    let idsig_fd = ParcelFileDescriptor::new(idsig);
     service.createOrUpdateIdsigFile(&apk_fd, &idsig_fd)?;
 
-    let idsig_file = File::open(idsig).context("Failed to open idsig file")?;
-    let idsig_fd = ParcelFileDescriptor::new(idsig_file);
+    let idsig = File::open(&config.idsig).context("Failed to open idsig file")?;
+    let idsig_fd = ParcelFileDescriptor::new(idsig);
 
-    if !instance.exists() {
+    if !config.instance.exists() {
         const INSTANCE_FILE_SIZE: u64 = 10 * 1024 * 1024;
         command_create_partition(
-            service,
-            instance,
+            service.as_ref(),
+            &config.instance,
             INSTANCE_FILE_SIZE,
             PartitionType::ANDROID_VM_INSTANCE,
         )?;
     }
 
-    let storage = if let Some(path) = storage {
+    let storage = if let Some(path) = config.microdroid.storage {
         if !path.exists() {
             command_create_partition(
-                service,
-                path,
-                storage_size.unwrap_or(10 * 1024 * 1024),
+                service.as_ref(),
+                &path,
+                config.microdroid.storage_size.unwrap_or(10 * 1024 * 1024),
                 PartitionType::ENCRYPTEDSTORE,
             )?;
         }
-        Some(open_parcel_file(path, true)?)
+        Some(open_parcel_file(&path, true)?)
     } else {
         None
     };
 
-    let kernel = kernel.map(|p| open_parcel_file(p, false)).transpose()?;
+    let kernel =
+        config.microdroid.kernel.as_ref().map(|p| open_parcel_file(p, false)).transpose()?;
 
-    let vendor = vendor.map(|p| open_parcel_file(p, false)).transpose()?;
+    let vendor =
+        config.microdroid.vendor.as_ref().map(|p| open_parcel_file(p, false)).transpose()?;
 
-    let extra_idsig_files: Result<Vec<File>, _> = extra_idsigs.iter().map(File::open).collect();
+    let extra_idsig_files: Result<Vec<File>, _> =
+        config.extra_idsigs.iter().map(File::open).collect();
     let extra_idsig_fds = extra_idsig_files?.into_iter().map(ParcelFileDescriptor::new).collect();
 
-    let payload = if let Some(config_path) = config_path {
-        if payload_binary_name.is_some() {
+    let payload = if let Some(config_path) = config.config_path {
+        if config.payload_binary_name.is_some() {
             bail!("Only one of --config-path or --payload-binary-name can be defined")
         }
         Payload::ConfigPath(config_path)
-    } else if let Some(payload_binary_name) = payload_binary_name {
+    } else if let Some(payload_binary_name) = config.payload_binary_name {
         Payload::PayloadConfig(VirtualMachinePayloadConfig {
             payloadBinaryName: payload_binary_name,
         })
@@ -142,14 +121,16 @@
         bail!("Either --config-path or --payload-binary-name must be defined")
     };
 
-    let payload_config_str = format!("{:?}!{:?}", apk, payload);
+    let payload_config_str = format!("{:?}!{:?}", config.apk, payload);
 
     let custom_config = CustomConfig {
         customKernelImage: kernel,
-        gdbPort: gdb.map(u16::from).unwrap_or(0) as i32, // 0 means no gdb
-        taskProfiles: task_profiles,
+        gdbPort: config.debug.gdb.map(u16::from).unwrap_or(0) as i32, // 0 means no gdb
+        taskProfiles: config.common.task_profiles,
         vendorImage: vendor,
-        devices: devices
+        devices: config
+            .microdroid
+            .devices
             .iter()
             .map(|x| {
                 x.to_str().map(String::from).ok_or(anyhow!("Failed to convert {x:?} to String"))
@@ -157,21 +138,28 @@
             .collect::<Result<_, _>>()?,
     };
 
-    let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
-        name: name.unwrap_or_else(|| String::from("VmRunApp")),
+    let vm_config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
+        name: config.common.name.unwrap_or_else(|| String::from("VmRunApp")),
         apk: apk_fd.into(),
         idsig: idsig_fd.into(),
         extraIdsigs: extra_idsig_fds,
-        instanceImage: open_parcel_file(instance, true /* writable */)?.into(),
+        instanceImage: open_parcel_file(&config.instance, true /* writable */)?.into(),
         encryptedStorageImage: storage,
         payload,
-        debugLevel: debug_level,
-        protectedVm: protected,
-        memoryMib: mem.unwrap_or(0) as i32, // 0 means use the VM default
-        cpuTopology: cpu_topology,
+        debugLevel: config.debug.debug,
+        protectedVm: config.common.protected,
+        memoryMib: config.common.mem.unwrap_or(0) as i32, // 0 means use the VM default
+        cpuTopology: config.common.cpu_topology,
         customConfig: Some(custom_config),
     });
-    run(service, &config, &payload_config_str, console_out_path, console_in_path, log_path)
+    run(
+        service.as_ref(),
+        &vm_config,
+        &payload_config_str,
+        config.debug.console.as_ref().map(|p| p.as_ref()),
+        config.debug.console_in.as_ref().map(|p| p.as_ref()),
+        config.debug.log.as_ref().map(|p| p.as_ref()),
+    )
 }
 
 fn find_empty_payload_apk_path() -> Result<PathBuf, Error> {
@@ -197,100 +185,55 @@
 }
 
 /// Run a VM with Microdroid
-#[allow(clippy::too_many_arguments)]
-pub fn command_run_microdroid(
-    name: Option<String>,
-    service: &dyn IVirtualizationService,
-    work_dir: Option<PathBuf>,
-    storage: Option<&Path>,
-    storage_size: Option<u64>,
-    console_out_path: Option<&Path>,
-    console_in_path: Option<&Path>,
-    log_path: Option<&Path>,
-    debug_level: DebugLevel,
-    protected: bool,
-    mem: Option<u32>,
-    cpu_topology: CpuTopology,
-    task_profiles: Vec<String>,
-    gdb: Option<NonZeroU16>,
-    kernel: Option<&Path>,
-    vendor: Option<&Path>,
-    devices: Vec<PathBuf>,
-) -> Result<(), Error> {
+pub fn command_run_microdroid(config: RunMicrodroidConfig) -> Result<(), Error> {
     let apk = find_empty_payload_apk_path()?;
     println!("found path {}", apk.display());
 
-    let work_dir = work_dir.unwrap_or(create_work_dir()?);
+    let work_dir = config.work_dir.unwrap_or(create_work_dir()?);
     let idsig = work_dir.join("apk.idsig");
     println!("apk.idsig path: {}", idsig.display());
     let instance_img = work_dir.join("instance.img");
     println!("instance.img path: {}", instance_img.display());
 
-    let payload_binary_name = "MicrodroidEmptyPayloadJniLib.so";
-    let extra_sig = [];
-    command_run_app(
-        name,
-        service,
-        &apk,
-        &idsig,
-        &instance_img,
-        storage,
-        storage_size,
-        /* config_path= */ None,
-        Some(payload_binary_name.to_owned()),
-        console_out_path,
-        console_in_path,
-        log_path,
-        debug_level,
-        protected,
-        mem,
-        cpu_topology,
-        task_profiles,
-        &extra_sig,
-        gdb,
-        kernel,
-        vendor,
-        devices,
-    )
+    let app_config = RunAppConfig {
+        common: config.common,
+        debug: config.debug,
+        microdroid: config.microdroid,
+        apk,
+        idsig,
+        instance: instance_img,
+        config_path: None,
+        payload_binary_name: Some("MicrodroidEmptyPayloadJniLib.so".to_owned()),
+        extra_idsigs: [].to_vec(),
+    };
+    command_run_app(app_config)
 }
 
 /// Run a VM from the given configuration file.
-#[allow(clippy::too_many_arguments)]
-pub fn command_run(
-    name: Option<String>,
-    service: &dyn IVirtualizationService,
-    config_path: &Path,
-    console_out_path: Option<&Path>,
-    console_in_path: Option<&Path>,
-    log_path: Option<&Path>,
-    mem: Option<u32>,
-    cpu_topology: CpuTopology,
-    task_profiles: Vec<String>,
-    gdb: Option<NonZeroU16>,
-) -> Result<(), Error> {
-    let config_file = File::open(config_path).context("Failed to open config file")?;
-    let mut config =
+pub fn command_run(config: RunCustomVmConfig) -> Result<(), Error> {
+    let config_file = File::open(&config.config).context("Failed to open config file")?;
+    let mut vm_config =
         VmConfig::load(&config_file).context("Failed to parse config file")?.to_parcelable()?;
-    if let Some(mem) = mem {
-        config.memoryMib = mem as i32;
+    if let Some(mem) = config.common.mem {
+        vm_config.memoryMib = mem as i32;
     }
-    if let Some(name) = name {
-        config.name = name;
+    if let Some(name) = config.common.name {
+        vm_config.name = name;
     } else {
-        config.name = String::from("VmRun");
+        vm_config.name = String::from("VmRun");
     }
-    if let Some(gdb) = gdb {
-        config.gdbPort = gdb.get() as i32;
+    if let Some(gdb) = config.debug.gdb {
+        vm_config.gdbPort = gdb.get() as i32;
     }
-    config.cpuTopology = cpu_topology;
-    config.taskProfiles = task_profiles;
+    vm_config.cpuTopology = config.common.cpu_topology;
+    vm_config.taskProfiles = config.common.task_profiles;
     run(
-        service,
-        &VirtualMachineConfig::RawConfig(config),
-        &format!("{:?}", config_path),
-        console_out_path,
-        console_in_path,
-        log_path,
+        get_service()?.as_ref(),
+        &VirtualMachineConfig::RawConfig(vm_config),
+        &format!("{:?}", &config.config),
+        config.debug.console.as_ref().map(|p| p.as_ref()),
+        config.debug.console_in.as_ref().map(|p| p.as_ref()),
+        config.debug.log.as_ref().map(|p| p.as_ref()),
     )
 }
 
diff --git a/vm_payload/Android.bp b/vm_payload/Android.bp
index ae0d1a6..49b7f5f 100644
--- a/vm_payload/Android.bp
+++ b/vm_payload/Android.bp
@@ -10,8 +10,6 @@
     srcs: ["src/*.rs"],
     include_dirs: ["include"],
     prefer_rlib: true,
-    // Require unsafe blocks for inside unsafe functions.
-    flags: ["-Dunsafe_op_in_unsafe_fn"],
     rustlibs: [
         "android.system.virtualization.payload-rust",
         "libandroid_logger",
diff --git a/vmbase/example/src/main.rs b/vmbase/example/src/main.rs
index a6f3bfa..ebd981c 100644
--- a/vmbase/example/src/main.rs
+++ b/vmbase/example/src/main.rs
@@ -16,8 +16,6 @@
 
 #![no_main]
 #![no_std]
-#![deny(unsafe_op_in_unsafe_fn)]
-#![deny(clippy::undocumented_unsafe_blocks)]
 
 mod exceptions;
 mod layout;
diff --git a/vmbase/src/lib.rs b/vmbase/src/lib.rs
index ca8756d..431e899 100644
--- a/vmbase/src/lib.rs
+++ b/vmbase/src/lib.rs
@@ -15,8 +15,6 @@
 //! Basic functionality for bare-metal binaries to run in a VM under crosvm.
 
 #![no_std]
-#![deny(unsafe_op_in_unsafe_fn)]
-#![deny(clippy::undocumented_unsafe_blocks)]
 
 extern crate alloc;
 
diff --git a/zipfuse/src/inode.rs b/zipfuse/src/inode.rs
index ef48389..3175a30 100644
--- a/zipfuse/src/inode.rs
+++ b/zipfuse/src/inode.rs
@@ -31,10 +31,11 @@
 const INVALID: Inode = 0;
 const ROOT: Inode = 1;
 
-const DEFAULT_DIR_MODE: u32 = libc::S_IRUSR | libc::S_IXUSR;
+const DEFAULT_DIR_MODE: u32 = libc::S_IRUSR | libc::S_IXUSR | libc::S_IRGRP | libc::S_IXGRP;
 // b/264668376 some files in APK don't have unix permissions specified. Default to 400
 // otherwise those files won't be readable even by the owner.
-const DEFAULT_FILE_MODE: u32 = libc::S_IRUSR;
+const DEFAULT_FILE_MODE: u32 = libc::S_IRUSR | libc::S_IRGRP;
+const EXECUTABLE_FILE_MODE: u32 = DEFAULT_FILE_MODE | libc::S_IXUSR | libc::S_IXGRP;
 
 /// `InodeData` represents an inode which has metadata about a file or a directory
 #[derive(Debug)]
@@ -191,7 +192,7 @@
                 // additional binaries that they might want to execute.
                 // An example of such binary is measure_io one used in the authfs performance tests.
                 // More context available at b/265261525 and b/270955654.
-                file_mode |= libc::S_IXUSR;
+                file_mode = EXECUTABLE_FILE_MODE;
             }
 
             while let Some(name) = iter.next() {