virtmgr: Pass VM DTBO path to crosvm if devices assigned

crosvm needs to know the path of VM DTBO to be able to populate the
guest device tree. Add a new method on the global VirtualizationService
which creates the file in a temporary directory common to all VMs and
has the file populated by VfioHandler.

Client virtmgr instances can then request a read-only FD for the file
from VirtualizationService. They then pass it as a preserved FD to
crosvm.

Bug: 297313212
Test: vm run-microdroid and see crosvm argument
Change-Id: I5491b9d084a8e845c1ad82d5cfad45b3374bf495
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 6ae3bbd..acd182c 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -482,7 +482,7 @@
             }
         };
 
-        let vfio_devices = if !config.devices.is_empty() {
+        let (vfio_devices, dtbo) = if !config.devices.is_empty() {
             let mut set = HashSet::new();
             for device in config.devices.iter() {
                 let path = canonicalize(device)
@@ -493,16 +493,25 @@
                         .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT);
                 }
             }
-            GLOBAL_SERVICE
+            let devices = GLOBAL_SERVICE
                 .bindDevicesToVfioDriver(&config.devices)?
                 .into_iter()
                 .map(|x| VfioDevice {
                     sysfs_path: PathBuf::from(&x.sysfsPath),
                     dtbo_label: x.dtboLabel,
                 })
-                .collect::<Vec<_>>()
+                .collect::<Vec<_>>();
+            let dtbo_file = File::from(
+                GLOBAL_SERVICE
+                    .getDtboFile()?
+                    .as_ref()
+                    .try_clone()
+                    .context("Failed to create File from ParcelFileDescriptor")
+                    .or_binder_exception(ExceptionCode::BAD_PARCELABLE)?,
+            );
+            (devices, Some(dtbo_file))
         } else {
-            vec![]
+            (vec![], None)
         };
 
         // Actually start the VM.
@@ -529,6 +538,7 @@
             detect_hangup: is_app_config,
             gdb_port,
             vfio_devices,
+            dtbo,
             dtbo_vendor,
         };
         let instance = Arc::new(
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 9a50776..2ba0e0e 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -116,6 +116,7 @@
     pub detect_hangup: bool,
     pub gdb_port: Option<NonZeroU16>,
     pub vfio_devices: Vec<VfioDevice>,
+    pub dtbo: Option<File>,
     pub dtbo_vendor: Option<File>,
 }
 
@@ -723,11 +724,23 @@
     }
 }
 
-fn append_platform_devices(command: &mut Command, config: &CrosvmConfig) -> Result<(), Error> {
+fn append_platform_devices(
+    command: &mut Command,
+    preserved_fds: &mut Vec<RawFd>,
+    config: &CrosvmConfig,
+) -> Result<(), Error> {
+    if config.vfio_devices.is_empty() {
+        return Ok(());
+    }
+
+    let Some(dtbo) = &config.dtbo else {
+        bail!("VFIO devices assigned but no DTBO available");
+    };
+    command.arg(format!("--device-tree-overlay={},filter", add_preserved_fd(preserved_fds, dtbo)));
+
     for device in &config.vfio_devices {
         command.arg(vfio_argument_for_platform_device(device)?);
     }
-    // TODO(b/291192693): add dtbo to command line when assigned device is not empty.
     Ok(())
 }
 
@@ -889,7 +902,7 @@
 
     // TODO(b/285855436): Pass dtbo_vendor after --device-tree-overlay crosvm option is supported.
 
-    append_platform_devices(&mut command, &config)?;
+    append_platform_devices(&mut command, &mut preserved_fds, &config)?;
 
     debug!("Preserving FDs {:?}", preserved_fds);
     command.preserved_fds(preserved_fds);
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index c384a6f..a2cb693 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -81,4 +81,7 @@
      * @return a list of pairs (sysfs path, DTBO node label) for devices.
      */
     BoundDevice[] bindDevicesToVfioDriver(in String[] devices);
+
+    /** Returns a read-only file descriptor of the VM DTBO file. */
+    ParcelFileDescriptor getDtboFile();
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 938225e..7cdfdc6 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -43,7 +43,7 @@
 use rustutils::system_properties;
 use serde::Deserialize;
 use std::collections::{HashMap, HashSet};
-use std::fs::{self, create_dir, remove_dir_all, set_permissions, File, Permissions};
+use std::fs::{self, create_dir, remove_dir_all, remove_file, set_permissions, File, Permissions};
 use std::io::{Read, Write};
 use std::os::unix::fs::PermissionsExt;
 use std::os::unix::raw::{pid_t, uid_t};
@@ -212,18 +212,8 @@
 
         let vfio_service: Strong<dyn IVfioHandler> =
             wait_for_interface(<BpVfioHandler as IVfioHandler>::get_descriptor())?;
-
         vfio_service.bindDevicesToVfioDriver(devices)?;
 
-        let dtbo_path = Path::new(TEMPORARY_DIRECTORY).join("common").join("dtbo");
-        if !dtbo_path.exists() {
-            // open a writable file descriptor for vfio_handler
-            let dtbo = File::create(&dtbo_path)
-                .context("Failed to create VM DTBO file")
-                .or_service_specific_exception(-1)?;
-            vfio_service.writeVmDtbo(&ParcelFileDescriptor::new(dtbo))?;
-        }
-
         Ok(get_assignable_devices()?
             .device
             .into_iter()
@@ -236,6 +226,14 @@
             })
             .collect::<Vec<_>>())
     }
+
+    fn getDtboFile(&self) -> binder::Result<ParcelFileDescriptor> {
+        check_use_custom_virtual_machine()?;
+
+        let state = &mut *self.state.lock().unwrap();
+        let file = state.get_dtbo_file().or_service_specific_exception(-1)?;
+        Ok(ParcelFileDescriptor::new(file))
+    }
 }
 
 // KEEP IN SYNC WITH assignable_devices.xsd
@@ -314,6 +312,9 @@
     /// VM contexts currently allocated to running VMs. A CID is never recycled as long
     /// as there is a strong reference held by a GlobalVmContext.
     held_contexts: HashMap<Cid, Weak<GlobalVmInstance>>,
+
+    /// Cached read-only FD of VM DTBO file. Also serves as a lock for creating the file.
+    dtbo_file: Mutex<Option<File>>,
 }
 
 impl GlobalState {
@@ -377,26 +378,64 @@
 
         let cid = self.get_next_available_cid()?;
         let instance = Arc::new(GlobalVmInstance { cid, requester_uid, requester_debug_pid });
-        create_temporary_directory(&instance.get_temp_dir(), requester_uid)?;
+        create_temporary_directory(&instance.get_temp_dir(), Some(requester_uid))?;
 
         self.held_contexts.insert(cid, Arc::downgrade(&instance));
         let binder = GlobalVmContext { instance, ..Default::default() };
         Ok(BnGlobalVmContext::new_binder(binder, BinderFeatures::default()))
     }
+
+    fn get_dtbo_file(&mut self) -> Result<File> {
+        let mut file = self.dtbo_file.lock().unwrap();
+
+        let fd = if let Some(ref_fd) = &*file {
+            ref_fd.try_clone()?
+        } else {
+            let path = get_or_create_common_dir()?.join("vm.dtbo");
+            if path.exists() {
+                // All temporary files are deleted when the service is started.
+                // If the file exists but the FD is not cached, the file is
+                // likely corrupted.
+                remove_file(&path).context("Failed to clone cached VM DTBO file descriptor")?;
+            }
+
+            // Open a write-only file descriptor for vfio_handler.
+            let write_fd = File::create(&path).context("Failed to create VM DTBO file")?;
+
+            let vfio_service: Strong<dyn IVfioHandler> =
+                wait_for_interface(<BpVfioHandler as IVfioHandler>::get_descriptor())?;
+            vfio_service.writeVmDtbo(&ParcelFileDescriptor::new(write_fd))?;
+
+            // Open read-only. This FD will be cached and returned to clients.
+            let read_fd = File::open(&path).context("Failed to open VM DTBO file")?;
+            let read_fd_clone =
+                read_fd.try_clone().context("Failed to clone VM DTBO file descriptor")?;
+            *file = Some(read_fd);
+            read_fd_clone
+        };
+
+        Ok(fd)
+    }
 }
 
-fn create_temporary_directory(path: &PathBuf, requester_uid: uid_t) -> Result<()> {
+fn create_temporary_directory(path: &PathBuf, requester_uid: Option<uid_t>) -> Result<()> {
+    // Directory may exist if previous attempt to create it had failed.
+    // Delete it before trying again.
     if path.as_path().exists() {
         remove_temporary_dir(path).unwrap_or_else(|e| {
             warn!("Could not delete temporary directory {:?}: {}", path, e);
         });
     }
-    // Create a directory that is owned by client's UID but system's GID, and permissions 0700.
+    // Create directory.
+    create_dir(path).with_context(|| format!("Could not create temporary directory {:?}", path))?;
+    // If provided, change ownership to client's UID but system's GID, and permissions 0700.
     // If the chown() fails, this will leave behind an empty directory that will get removed
     // at the next attempt, or if virtualizationservice is restarted.
-    create_dir(path).with_context(|| format!("Could not create temporary directory {:?}", path))?;
-    chown(path, Some(Uid::from_raw(requester_uid)), None)
-        .with_context(|| format!("Could not set ownership of temporary directory {:?}", path))?;
+    if let Some(uid) = requester_uid {
+        chown(path, Some(Uid::from_raw(uid)), None).with_context(|| {
+            format!("Could not set ownership of temporary directory {:?}", path)
+        })?;
+    }
     Ok(())
 }
 
@@ -410,6 +449,14 @@
     Ok(())
 }
 
+fn get_or_create_common_dir() -> Result<PathBuf> {
+    let path = Path::new(TEMPORARY_DIRECTORY).join("common");
+    if !path.exists() {
+        create_temporary_directory(&path, None)?;
+    }
+    Ok(path)
+}
+
 /// Implementation of the AIDL `IGlobalVmContext` interface.
 #[derive(Debug, Default)]
 struct GlobalVmContext {