Merge changes from topic "virtualizationservice_bind_device_to_vfio" into main

* changes:
  Bind devices to VFIO with VirtualizationService
  Implement platform API to assign devices
diff --git a/apex/Android.bp b/apex/Android.bp
index 7ef2c79..765372a 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -76,6 +76,7 @@
         arm64: {
             binaries: [
                 "crosvm",
+                "vfio_handler",
                 "virtmgr",
                 "virtualizationservice",
             ],
@@ -84,6 +85,7 @@
         x86_64: {
             binaries: [
                 "crosvm",
+                "vfio_handler",
                 "virtmgr",
                 "virtualizationservice",
             ],
diff --git a/apex/virtualizationservice.rc b/apex/virtualizationservice.rc
index 02b2081..be90904 100644
--- a/apex/virtualizationservice.rc
+++ b/apex/virtualizationservice.rc
@@ -19,3 +19,10 @@
     interface aidl android.system.virtualizationservice
     disabled
     oneshot
+
+service vfio_handler /apex/com.android.virt/bin/vfio_handler
+    user root
+    group root
+    interface aidl android.system.virtualizationservice_internal.IVfioHandler
+    disabled
+    oneshot
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 4cad2e3..b307854 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -62,6 +62,8 @@
 public final class VirtualMachineConfig {
     private static final String TAG = "VirtualMachineConfig";
 
+    private static String[] EMPTY_STRING_ARRAY = {};
+
     // These define the schema of the config file persisted on disk.
     private static final int VERSION = 6;
     private static final String KEY_VERSION = "version";
@@ -517,7 +519,8 @@
         if (mVendorDiskImage != null) {
             VirtualMachineAppConfig.CustomConfig customConfig =
                     new VirtualMachineAppConfig.CustomConfig();
-            customConfig.taskProfiles = new String[0];
+            customConfig.taskProfiles = EMPTY_STRING_ARRAY;
+            customConfig.devices = EMPTY_STRING_ARRAY;
             try {
                 customConfig.vendorImage =
                         ParcelFileDescriptor.open(mVendorDiskImage, MODE_READ_ONLY);
diff --git a/libs/vmconfig/src/lib.rs b/libs/vmconfig/src/lib.rs
index 7ca8272..50f3c8e 100644
--- a/libs/vmconfig/src/lib.rs
+++ b/libs/vmconfig/src/lib.rs
@@ -21,7 +21,7 @@
     binder::ParcelFileDescriptor,
 };
 
-use anyhow::{bail, Context, Error, Result};
+use anyhow::{anyhow, bail, Context, Error, Result};
 use semver::VersionReq;
 use serde::{Deserialize, Serialize};
 use std::convert::TryInto;
@@ -57,6 +57,9 @@
     /// Version or range of versions of the virtual platform that this config is compatible with.
     /// The format follows SemVer (https://semver.org).
     pub platform_version: VersionReq,
+    /// SysFS paths of devices assigned to the VM.
+    #[serde(default)]
+    pub devices: Vec<PathBuf>,
 }
 
 impl VmConfig {
@@ -103,6 +106,13 @@
             protectedVm: self.protected,
             memoryMib: memory_mib,
             platformVersion: self.platform_version.to_string(),
+            devices: self
+                .devices
+                .iter()
+                .map(|x| {
+                    x.to_str().map(String::from).ok_or(anyhow!("Failed to convert {x:?} to String"))
+                })
+                .collect::<Result<_>>()?,
             ..Default::default()
         })
     }
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index 4ad8eb8..8089016 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -117,8 +117,8 @@
         memoryMib: 300,
         cpuTopology: CpuTopology::ONE_CPU,
         platformVersion: "~1.0".to_string(),
-        taskProfiles: vec![],
         gdbPort: 0, // No gdb
+        ..Default::default()
     });
     let vm = VmInstance::create(
         service.as_ref(),
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index daee7c5..15e5407 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -62,9 +62,10 @@
 use rpcbinder::RpcServer;
 use rustutils::system_properties;
 use semver::VersionReq;
+use std::collections::HashSet;
 use std::convert::TryInto;
 use std::ffi::CStr;
-use std::fs::{read_dir, remove_file, File, OpenOptions};
+use std::fs::{canonicalize, read_dir, remove_file, File, OpenOptions};
 use std::io::{BufRead, BufReader, Error, ErrorKind, Write};
 use std::num::{NonZeroU16, NonZeroU32};
 use std::os::unix::io::{FromRawFd, IntoRawFd};
@@ -337,7 +338,8 @@
                 // - controlling CPUs;
                 // - specifying a config file in the APK; (this one is not part of CustomConfig)
                 // - gdbPort is set, meaning that crosvm will start a gdb server;
-                // - using anything other than the default kernel.
+                // - using anything other than the default kernel;
+                // - specifying devices to be assigned.
                 config.customConfig.is_some() || matches!(config.payload, Payload::ConfigPath(_))
             }
         };
@@ -456,6 +458,27 @@
             }
         };
 
+        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:?}")),
+                    )
+                })?;
+                if !set.insert(path) {
+                    return Err(Status::new_exception_str(
+                        ExceptionCode::ILLEGAL_ARGUMENT,
+                        Some(format!("duplicated device {device}")),
+                    ));
+                }
+            }
+            Some(clone_file(&GLOBAL_SERVICE.bindDevicesToVfioDriver(&config.devices)?)?)
+        } else {
+            None
+        };
+
         // Actually start the VM.
         let crosvm_config = CrosvmConfig {
             cid,
@@ -479,6 +502,8 @@
             platform_version: parse_platform_version_req(&config.platformVersion)?,
             detect_hangup: is_app_config,
             gdb_port,
+            vfio_devices: config.devices.iter().map(PathBuf::from).collect(),
+            devices_dtbo,
         };
         let instance = Arc::new(
             VmInstance::new(
@@ -641,6 +666,8 @@
             add_microdroid_vendor_image(clone_file(file)?, &mut vm_config);
             append_kernel_param("androidboot.microdroid.mount_vendor=1", &mut vm_config)
         }
+
+        vm_config.devices = custom_config.devices.clone();
     }
 
     if config.memoryMib > 0 {
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 31db3f6..68cc7f2 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -115,6 +115,8 @@
     pub platform_version: VersionReq,
     pub detect_hangup: bool,
     pub gdb_port: Option<NonZeroU16>,
+    pub vfio_devices: Vec<PathBuf>,
+    pub devices_dtbo: Option<File>,
 }
 
 /// A disk image to pass to crosvm for a VM.
@@ -185,6 +187,7 @@
         if let VmState::NotStarted { config } = state {
             let detect_hangup = config.detect_hangup;
             let (failure_pipe_read, failure_pipe_write) = create_pipe()?;
+            let vfio_devices = config.vfio_devices.clone();
 
             // If this fails and returns an error, `self` will be left in the `Failed` state.
             let child =
@@ -199,7 +202,7 @@
             let child_clone = child.clone();
             let instance_clone = instance.clone();
             let monitor_vm_exit_thread = Some(thread::spawn(move || {
-                instance_clone.monitor_vm_exit(child_clone, failure_pipe_read);
+                instance_clone.monitor_vm_exit(child_clone, failure_pipe_read, vfio_devices);
             }));
 
             if detect_hangup {
@@ -336,7 +339,12 @@
     /// Monitors the exit of the VM (i.e. termination of the `child` process). When that happens,
     /// handles the event by updating the state, noityfing the event to clients by calling
     /// callbacks, and removing temporary files for the VM.
-    fn monitor_vm_exit(&self, child: Arc<SharedChild>, mut failure_pipe_read: File) {
+    fn monitor_vm_exit(
+        &self,
+        child: Arc<SharedChild>,
+        mut failure_pipe_read: File,
+        vfio_devices: Vec<PathBuf>,
+    ) {
         let result = child.wait();
         match &result {
             Err(e) => error!("Error waiting for crosvm({}) instance to die: {}", child.id(), e),
@@ -394,6 +402,11 @@
         remove_temporary_files(&self.temporary_directory).unwrap_or_else(|e| {
             error!("Error removing temporary files from {:?}: {}", self.temporary_directory, e);
         });
+
+        // TODO(b/278008182): clean up assigned devices.
+        for device in vfio_devices.iter() {
+            info!("NOT RELEASING {device:?}");
+        }
     }
 
     /// Waits until payload is started, or timeout expires. When timeout occurs, kill
@@ -677,6 +690,39 @@
     }
 }
 
+const SYSFS_PLATFORM_DEVICES_PATH: &str = "/sys/devices/platform/";
+const VFIO_PLATFORM_DRIVER_PATH: &str = "/sys/bus/platform/drivers/vfio-platform";
+
+fn vfio_argument_for_platform_device(path: &Path) -> Result<String, Error> {
+    // Check platform device exists
+    let path = path.canonicalize()?;
+    if !path.starts_with(SYSFS_PLATFORM_DEVICES_PATH) {
+        bail!("{path:?} is not a platform device");
+    }
+
+    // Check platform device is bound to VFIO driver
+    let dev_driver_path = path.join("driver").canonicalize()?;
+    if dev_driver_path != Path::new(VFIO_PLATFORM_DRIVER_PATH) {
+        bail!("{path:?} is not bound to VFIO-platform driver");
+    }
+
+    if let Some(p) = path.to_str() {
+        Ok(format!("--vfio={p},iommu=viommu"))
+    } else {
+        bail!("invalid path {path:?}");
+    }
+}
+
+fn append_platform_devices(command: &mut Command, config: &CrosvmConfig) -> Result<(), Error> {
+    for device in &config.vfio_devices {
+        command.arg(vfio_argument_for_platform_device(device)?);
+    }
+    if let Some(_dtbo) = &config.devices_dtbo {
+        // TODO(b/291192693): add dtbo to command line
+    }
+    Ok(())
+}
+
 /// Starts an instance of `crosvm` to manage a new VM.
 fn run_vm(
     config: CrosvmConfig,
@@ -833,6 +879,8 @@
         .arg("--socket")
         .arg(add_preserved_fd(&mut preserved_fds, &control_server_socket.as_raw_descriptor()));
 
+    append_platform_devices(&mut command, &config)?;
+
     debug!("Preserving FDs {:?}", preserved_fds);
     command.preserved_fds(preserved_fds);
 
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 0732c04..6b39ff9 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -36,7 +36,6 @@
         "libstatslog_virtualization_rust",
         "libtombstoned_client_rust",
         "libvsock",
-        "liblazy_static",
     ],
     apex_available: ["com.android.virt"],
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index 2b762c4..9021055 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -108,6 +108,9 @@
 
         /** A disk image containing vendor specific modules. */
         @nullable ParcelFileDescriptor vendorImage;
+
+        /** List of SysFS nodes of devices to be assigned */
+        String[] devices;
     }
 
     /** Configuration parameters guarded by android.permission.USE_CUSTOM_VIRTUAL_MACHINE */
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 87d4ba2..7c0ed0c 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -69,4 +69,7 @@
      * If set to zero, then gdb server won't be started.
      */
     int gdbPort = 0;
+
+    /** List of SysFS nodes of devices to be assigned */
+    String[] devices;
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVfioHandler.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVfioHandler.aidl
new file mode 100644
index 0000000..516b7a1
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVfioHandler.aidl
@@ -0,0 +1,34 @@
+/*
+ * 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 android.system.virtualizationservice_internal;
+
+import android.system.virtualizationservice.AssignableDevice;
+import android.system.virtualizationservice.VirtualMachineDebugInfo;
+import android.system.virtualizationservice_internal.AtomVmBooted;
+import android.system.virtualizationservice_internal.AtomVmCreationRequested;
+import android.system.virtualizationservice_internal.AtomVmExited;
+import android.system.virtualizationservice_internal.IGlobalVmContext;
+
+/** VFIO related methods which should be done as root. */
+interface IVfioHandler {
+    /**
+     * Bind given devices to vfio driver.
+     *
+     * @param devices paths of sysfs nodes of devices to assign.
+     * @return a file descriptor containing DTBO for VM.
+     */
+    ParcelFileDescriptor bindDevicesToVfioDriver(in String[] devices);
+}
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 2dbb6e2..384915c 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -29,26 +29,24 @@
     AtomVmExited::AtomVmExited,
     IGlobalVmContext::{BnGlobalVmContext, IGlobalVmContext},
     IVirtualizationServiceInternal::IVirtualizationServiceInternal,
+    IVfioHandler::{BpVfioHandler, IVfioHandler},
 };
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::VM_TOMBSTONES_SERVICE_PORT;
 use anyhow::{anyhow, ensure, Context, Result};
-use binder::{self, BinderFeatures, ExceptionCode, Interface, LazyServiceGuard, Status, Strong};
-use lazy_static::lazy_static;
+use binder::{self, wait_for_interface, BinderFeatures, ExceptionCode, Interface, LazyServiceGuard, Status, Strong};
 use libc::VMADDR_CID_HOST;
 use log::{error, info, warn};
 use rustutils::system_properties;
-use std::collections::{HashMap, HashSet};
-use std::fs::{canonicalize, create_dir, remove_dir_all, set_permissions, File, Permissions};
+use std::collections::HashMap;
+use std::fs::{create_dir, remove_dir_all, set_permissions, Permissions};
 use std::io::{Read, Write};
-use std::os::fd::FromRawFd;
 use std::os::unix::fs::PermissionsExt;
 use std::os::unix::raw::{pid_t, uid_t};
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
 use std::sync::{Arc, Mutex, Weak};
 use tombstoned_client::{DebuggerdDumpType, TombstonedConnection};
 use vsock::{VsockListener, VsockStream};
-use nix::fcntl::OFlag;
-use nix::unistd::{chown, pipe2, Uid};
+use nix::unistd::{chown, Uid};
 
 /// The unique ID of a VM used (together with a port number) for vsock communication.
 pub type Cid = u32;
@@ -188,73 +186,12 @@
     fn bindDevicesToVfioDriver(&self, devices: &[String]) -> binder::Result<ParcelFileDescriptor> {
         check_use_custom_virtual_machine()?;
 
-        let mut set = HashSet::new();
-        for device in devices.iter() {
-            if !set.insert(device) {
-                return Err(Status::new_exception_str(
-                    ExceptionCode::ILLEGAL_ARGUMENT,
-                    Some(format!("duplicated device {device}")),
-                ));
-            }
-            bind_device(device)?;
-        }
-
-        // TODO(b/278008182): create a file descriptor containing DTBO for devices.
-        let (raw_read, raw_write) = pipe2(OFlag::O_CLOEXEC).map_err(|e| {
-            Status::new_exception_str(
-                ExceptionCode::SERVICE_SPECIFIC,
-                Some(format!("can't create fd for DTBO: {e:?}")),
-            )
-        })?;
-        // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
-        let read_fd = unsafe { File::from_raw_fd(raw_read) };
-        // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
-        let _write_fd = unsafe { File::from_raw_fd(raw_write) };
-
-        Ok(ParcelFileDescriptor::new(read_fd))
+        let vfio_service: Strong<dyn IVfioHandler> =
+            wait_for_interface(<BpVfioHandler as IVfioHandler>::get_descriptor())?;
+        vfio_service.bindDevicesToVfioDriver(devices)
     }
 }
 
-lazy_static! {
-    static ref SYSFS_PLATFORM_DEVICES: &'static Path = Path::new("/sys/devices/platform/");
-    static ref VFIO_PLATFORM_DRIVER: &'static Path =
-        Path::new("/sys/bus/platform/drivers/vfio-platform");
-}
-
-fn bind_device(device: &str) -> binder::Result<()> {
-    // Check platform device exists
-    let dev_sysfs_path = canonicalize(device).map_err(|e| {
-        Status::new_exception_str(
-            ExceptionCode::SERVICE_SPECIFIC,
-            Some(format!("can't canonicalize: {e:?}")),
-        )
-    })?;
-    if !dev_sysfs_path.starts_with(*SYSFS_PLATFORM_DEVICES) {
-        return Err(Status::new_exception_str(
-            ExceptionCode::ILLEGAL_ARGUMENT,
-            Some(format!("{device} is not a platform device")),
-        ));
-    }
-
-    // Check platform device is bound to VFIO driver
-    let dev_driver_path = canonicalize(dev_sysfs_path.join("driver")).map_err(|e| {
-        Status::new_exception_str(
-            ExceptionCode::SERVICE_SPECIFIC,
-            Some(format!("can't canonicalize: {e:?}")),
-        )
-    })?;
-    if dev_driver_path != *VFIO_PLATFORM_DRIVER {
-        // TODO(b/278008182): unbind driver and bind to VFIO
-        return Err(Status::new_exception_str(
-            ExceptionCode::UNSUPPORTED_OPERATION,
-            Some("not implemented".to_owned()),
-        ));
-    }
-
-    // already bound to VFIO driver
-    Ok(())
-}
-
 #[derive(Debug, Default)]
 struct GlobalVmInstance {
     /// The unique CID assigned to the VM for vsock communication.
diff --git a/virtualizationservice/src/rkpvm.rs b/virtualizationservice/src/rkpvm.rs
index ebfb667..63160f4 100644
--- a/virtualizationservice/src/rkpvm.rs
+++ b/virtualizationservice/src/rkpvm.rs
@@ -76,8 +76,8 @@
         memoryMib: 300,
         cpuTopology: CpuTopology::ONE_CPU,
         platformVersion: "~1.0".to_string(),
-        taskProfiles: vec![],
         gdbPort: 0, // No gdb
+        ..Default::default()
     });
     let vm = VmInstance::create(service.as_ref(), &config, None, None, None, None)
         .context("Failed to create service VM")?;
diff --git a/virtualizationservice/vfio_handler/Android.bp b/virtualizationservice/vfio_handler/Android.bp
new file mode 100644
index 0000000..efbb7b5
--- /dev/null
+++ b/virtualizationservice/vfio_handler/Android.bp
@@ -0,0 +1,31 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_binary {
+    name: "vfio_handler",
+    crate_name: "vfio_handler",
+    edition: "2021",
+    srcs: ["src/main.rs"],
+    // Only build on targets which crosvm builds on.
+    enabled: false,
+    target: {
+        android64: {
+            compile_multilib: "64",
+            enabled: true,
+        },
+        linux_bionic_arm64: {
+            enabled: true,
+        },
+    },
+    prefer_rlib: true,
+    rustlibs: [
+        "android.system.virtualizationservice_internal-rust",
+        "libandroid_logger",
+        "libbinder_rs",
+        "liblog_rust",
+        "libnix",
+        "liblazy_static",
+    ],
+    apex_available: ["com.android.virt"],
+}
diff --git a/virtualizationservice/vfio_handler/src/aidl.rs b/virtualizationservice/vfio_handler/src/aidl.rs
new file mode 100644
index 0000000..9a50fd3
--- /dev/null
+++ b/virtualizationservice/vfio_handler/src/aidl.rs
@@ -0,0 +1,182 @@
+// 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.
+
+//! Implementation of the AIDL interface of the VirtualizationService.
+
+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 lazy_static::lazy_static;
+use std::fs::{read_link, write, File};
+use std::os::fd::FromRawFd;
+use std::path::Path;
+use nix::fcntl::OFlag;
+use nix::unistd::pipe2;
+
+#[derive(Debug, Default)]
+pub struct VfioHandler {}
+
+impl VfioHandler {
+    pub fn init() -> VfioHandler {
+        VfioHandler::default()
+    }
+}
+
+impl Interface for VfioHandler {}
+
+impl IVfioHandler for VfioHandler {
+    fn bindDevicesToVfioDriver(&self, devices: &[String]) -> binder::Result<ParcelFileDescriptor> {
+        // 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"),
+            ));
+        }
+
+        devices.iter().try_for_each(|x| bind_device(Path::new(x)))?;
+
+        // TODO(b/278008182): create a file descriptor containing DTBO for devices.
+        let (raw_read, raw_write) = pipe2(OFlag::O_CLOEXEC).map_err(|e| {
+            Status::new_exception_str(
+                ExceptionCode::SERVICE_SPECIFIC,
+                Some(format!("can't create fd for DTBO: {e:?}")),
+            )
+        })?;
+        // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
+        let read_fd = unsafe { File::from_raw_fd(raw_read) };
+        // SAFETY: We are the sole owner of this FD as we just created it, and it is valid and open.
+        let _write_fd = unsafe { File::from_raw_fd(raw_write) };
+
+        Ok(ParcelFileDescriptor::new(read_fd))
+    }
+}
+
+const DEV_VFIO_PATH: &str = "/dev/vfio/vfio";
+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";
+
+lazy_static! {
+    static ref IS_VFIO_SUPPORTED: bool = is_vfio_supported();
+}
+
+fn 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:?}")),
+        ));
+    }
+
+    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")),
+        ));
+    }
+
+    Ok(())
+}
+
+fn get_device_iommu_group(path: &Path) -> Option<u64> {
+    let group_path = read_link(path.join("iommu_group")).ok()?;
+    let group = group_path.file_name()?;
+    group.to_str()?.parse().ok()
+}
+
+fn is_bound_to_vfio_driver(path: &Path) -> bool {
+    let Ok(driver_path) = read_link(path.join("driver")) else {
+        return false;
+    };
+    let Some(driver) = driver_path.file_name() else {
+        return false;
+    };
+    driver.to_str().unwrap_or("") == "vfio-platform"
+}
+
+fn bind_vfio_driver(path: &Path) -> binder::Result<()> {
+    if is_bound_to_vfio_driver(path) {
+        // already bound
+        return Ok(());
+    }
+
+    // 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:?}"))
+        ));
+    };
+    let Some(device_str) = device.to_str() else {
+        return Err(Status::new_exception_str(
+            ExceptionCode::ILLEGAL_ARGUMENT,
+            Some(format!("invalid filename {device:?}"))
+        ));
+    };
+    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:?}")),
+        )
+    })?;
+
+    // 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(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:?}")),
+        )
+    })?;
+
+    // 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")),
+        ));
+    }
+
+    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:?}")),
+        ));
+    }
+
+    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:?}")),
+        )
+    })?;
+
+    check_platform_device(&path)?;
+    bind_vfio_driver(&path)
+}
diff --git a/virtualizationservice/vfio_handler/src/main.rs b/virtualizationservice/vfio_handler/src/main.rs
new file mode 100644
index 0000000..1a1cce8
--- /dev/null
+++ b/virtualizationservice/vfio_handler/src/main.rs
@@ -0,0 +1,45 @@
+// 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.
+
+//! Android VfioHandler
+
+mod aidl;
+
+use crate::aidl::VfioHandler;
+use android_logger::Config;
+use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IVfioHandler::{
+    BnVfioHandler,
+    BpVfioHandler,
+    IVfioHandler,
+};
+use binder::{register_lazy_service, BinderFeatures, ProcessState};
+use log::{info, Level};
+
+const LOG_TAG: &str = "VfioHandler";
+
+fn main() {
+    android_logger::init_once(
+        Config::default()
+            .with_tag(LOG_TAG)
+            .with_min_level(Level::Info)
+            .with_log_id(android_logger::LogId::System),
+    );
+
+    let service = VfioHandler::init();
+    let service = BnVfioHandler::new_binder(service, BinderFeatures::default());
+    register_lazy_service(<BpVfioHandler as IVfioHandler>::get_descriptor(), service.as_binder())
+        .unwrap();
+    info!("Registered Binder service, joining threadpool.");
+    ProcessState::join_thread_pool();
+}
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 0c99acb..64bcd02 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -119,6 +119,10 @@
         /// 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>,
     },
     /// Run a virtual machine with Microdroid inside
     RunMicrodroid {
@@ -187,6 +191,10 @@
         /// 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>,
     },
     /// Run a virtual machine
     Run {
@@ -308,6 +316,7 @@
             gdb,
             kernel,
             vendor,
+            devices,
         } => command_run_app(
             name,
             get_service()?.as_ref(),
@@ -330,6 +339,7 @@
             gdb,
             kernel.as_deref(),
             vendor.as_deref(),
+            devices,
         ),
         Opt::RunMicrodroid {
             name,
@@ -347,6 +357,7 @@
             gdb,
             kernel,
             vendor,
+            devices,
         } => command_run_microdroid(
             name,
             get_service()?.as_ref(),
@@ -364,6 +375,7 @@
             gdb,
             kernel.as_deref(),
             vendor.as_deref(),
+            devices,
         ),
         Opt::Run { name, config, cpu_topology, task_profiles, console, console_in, log, gdb } => {
             command_run(
@@ -420,6 +432,18 @@
         println!("/dev/kvm does not exist.");
     }
 
+    if Path::new("/dev/vfio/vfio").exists() {
+        println!("/dev/vfio/vfio exists.");
+    } else {
+        println!("/dev/vfio/vfio does not exist.");
+    }
+
+    if Path::new("/sys/bus/platform/drivers/vfio-platform").exists() {
+        println!("VFIO-platform is supported.");
+    } else {
+        println!("VFIO-platform is not supported.");
+    }
+
     Ok(())
 }
 
diff --git a/vm/src/run.rs b/vm/src/run.rs
index f50bd50..250c56c 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -66,6 +66,7 @@
     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")?;
 
@@ -148,6 +149,12 @@
         gdbPort: gdb.map(u16::from).unwrap_or(0) as i32, // 0 means no gdb
         taskProfiles: task_profiles,
         vendorImage: vendor,
+        devices: devices
+            .iter()
+            .map(|x| {
+                x.to_str().map(String::from).ok_or(anyhow!("Failed to convert {x:?} to String"))
+            })
+            .collect::<Result<_, _>>()?,
     };
 
     let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
@@ -208,6 +215,7 @@
     gdb: Option<NonZeroU16>,
     kernel: Option<&Path>,
     vendor: Option<&Path>,
+    devices: Vec<PathBuf>,
 ) -> Result<(), Error> {
     let apk = find_empty_payload_apk_path()?;
     println!("found path {}", apk.display());
@@ -242,6 +250,7 @@
         gdb,
         kernel,
         vendor,
+        devices,
     )
 }
 
diff --git a/vmbase/example/tests/test.rs b/vmbase/example/tests/test.rs
index 2b6dfbc..17ff947 100644
--- a/vmbase/example/tests/test.rs
+++ b/vmbase/example/tests/test.rs
@@ -101,8 +101,8 @@
         memoryMib: 300,
         cpuTopology: CpuTopology::ONE_CPU,
         platformVersion: "~1.0".to_string(),
-        taskProfiles: vec![],
         gdbPort: 0, // no gdb
+        ..Default::default()
     });
     let (handle, console) = android_log_fd()?;
     let (mut log_reader, log_writer) = pipe()?;