Build composite image in VirtualizationService.
Bug: 184131523
Test: atest VirtualizationTestCases
Test: ran microdroid manually
Change-Id: I24eb776bb3049a4cdc5f000607447a73bd0adeec
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 2c44200..2d78018 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -14,6 +14,8 @@
"libanyhow",
"libcommand_fds",
"liblog_rust",
+ "libserde_json",
+ "libserde",
"libshared_child",
],
apex_available: ["com.android.virt"],
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/DiskImage.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/DiskImage.aidl
index 6bc747e..ab4c37d 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/DiskImage.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/DiskImage.aidl
@@ -15,11 +15,18 @@
*/
package android.system.virtualizationservice;
+import android.system.virtualizationservice.Partition;
+
/** A disk image to be made available to the VM. */
parcelable DiskImage {
- /** The disk image. */
- ParcelFileDescriptor image;
+ /**
+ * The disk image, if it already exists. Exactly one of this and `partitions` must be specified.
+ */
+ @nullable ParcelFileDescriptor image;
/** Whether this disk should be writable by the VM. */
boolean writable;
+
+ /** Partition images to be assembled into a composite image. */
+ Partition[] partitions;
}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl
new file mode 100644
index 0000000..782c239
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 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;
+
+/** A partition to be assembled into a composite image. */
+parcelable Partition {
+ /** A label for the partition. */
+ String label;
+
+ /** The backing file descriptor of the partition image. */
+ ParcelFileDescriptor image;
+
+ /** Whether the partition should be writable by the VM. */
+ boolean writable;
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
index d20d91d..18b01ce 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
@@ -20,6 +20,9 @@
/** The CID assigned to the VM. */
int cid;
+ /** Directory of temporary files used by the VM while it is running. */
+ String temporaryDirectory;
+
/** The UID of the process which requested the VM. */
int requesterUid;
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index ef973d1..c295388 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -14,9 +14,11 @@
//! Implementation of the AIDL interface of the VirtualizationService.
-use crate::crosvm::VmInstance;
+use crate::composite::make_composite_image;
+use crate::crosvm::{CrosvmConfig, DiskFile, VmInstance};
use crate::{Cid, FIRST_GUEST_CID};
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::IVirtualizationService;
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::DiskImage::DiskImage;
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualMachine::{
BnVirtualMachine, IVirtualMachine,
};
@@ -26,11 +28,18 @@
use android_system_virtualizationservice::binder::{
self, BinderFeatures, Interface, ParcelFileDescriptor, StatusCode, Strong, ThreadState,
};
-use log::{debug, error};
+use command_fds::FdMapping;
+use log::{debug, error, warn};
+use std::fs::{File, create_dir};
+use std::os::unix::io::AsRawFd;
+use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, Weak};
pub const BINDER_SERVICE_IDENTIFIER: &str = "android.system.virtualizationservice";
+/// Directory in which to write disk image files used while running VMs.
+const TEMPORARY_DIRECTORY: &str = "/data/misc/virtualizationservice";
+
// TODO(qwandor): Use PermissionController once it is available to Rust.
/// Only processes running with one of these UIDs are allowed to call debug methods.
const DEBUG_ALLOWED_UIDS: [u32; 2] = [0, 2000];
@@ -53,30 +62,63 @@
log_fd: Option<&ParcelFileDescriptor>,
) -> binder::Result<Strong<dyn IVirtualMachine>> {
let state = &mut *self.state.lock().unwrap();
- let log_fd = log_fd
- .map(|fd| fd.as_ref().try_clone().map_err(|_| StatusCode::UNKNOWN_ERROR))
- .transpose()?;
+ let log_fd = log_fd.map(clone_file).transpose()?;
let requester_uid = ThreadState::get_calling_uid();
- let requester_sid = ThreadState::with_calling_sid(|sid| {
- if let Some(sid) = sid {
- match sid.to_str() {
- Ok(sid) => Ok(sid.to_owned()),
- Err(e) => {
- error!("SID was not valid UTF-8: {:?}", e);
- Err(StatusCode::BAD_VALUE)
- }
- }
- } else {
- error!("Missing SID on startVm");
- Err(StatusCode::UNKNOWN_ERROR)
- }
- })?;
+ let requester_sid = get_calling_sid()?;
let requester_debug_pid = ThreadState::get_calling_pid();
let cid = state.allocate_cid()?;
- let instance = VmInstance::start(
- config,
+
+ // Counter to generate unique IDs for temporary image files.
+ let mut next_temporary_image_id = 0;
+ // Files which are referred to from composite images. These must be mapped to the crosvm
+ // child process, and not closed before it is started.
+ let mut indirect_files = vec![];
+
+ // Make directory for temporary files.
+ let temporary_directory: PathBuf = format!("{}/{}", TEMPORARY_DIRECTORY, cid).into();
+ create_dir(&temporary_directory).map_err(|e| {
+ error!(
+ "Failed to create temporary directory {:?} for VM files: {:?}",
+ temporary_directory, e
+ );
+ StatusCode::UNKNOWN_ERROR
+ })?;
+
+ // Assemble disk images if needed.
+ let disks = config
+ .disks
+ .iter()
+ .map(|disk| {
+ assemble_disk_image(
+ disk,
+ &temporary_directory,
+ &mut next_temporary_image_id,
+ &mut indirect_files,
+ )
+ })
+ .collect::<Result<Vec<DiskFile>, _>>()?;
+
+ // Actually start the VM.
+ let crosvm_config = CrosvmConfig {
cid,
+ bootloader: as_asref(&config.bootloader),
+ kernel: as_asref(&config.kernel),
+ initrd: as_asref(&config.initrd),
+ disks,
+ params: config.params.to_owned(),
+ };
+ let composite_disk_mappings: Vec<_> = indirect_files
+ .iter()
+ .map(|file| {
+ let fd = file.as_raw_fd();
+ FdMapping { parent_fd: fd, child_fd: fd }
+ })
+ .collect();
+ let instance = VmInstance::start(
+ &crosvm_config,
log_fd,
+ &composite_disk_mappings,
+ temporary_directory,
requester_uid,
requester_sid,
requester_debug_pid,
@@ -102,6 +144,7 @@
.into_iter()
.map(|vm| VirtualMachineDebugInfo {
cid: vm.cid as i32,
+ temporaryDirectory: vm.temporary_directory.to_string_lossy().to_string(),
requesterUid: vm.requester_uid as i32,
requesterSid: vm.requester_sid.clone(),
requesterPid: vm.requester_debug_pid,
@@ -136,6 +179,72 @@
}
}
+/// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
+///
+/// This may involve assembling a composite disk from a set of partition images.
+fn assemble_disk_image(
+ disk: &DiskImage,
+ temporary_directory: &Path,
+ next_temporary_image_id: &mut u64,
+ indirect_files: &mut Vec<File>,
+) -> Result<DiskFile, StatusCode> {
+ let image = if !disk.partitions.is_empty() {
+ if disk.image.is_some() {
+ warn!("DiskImage {:?} contains both image and partitions.", disk);
+ return Err(StatusCode::BAD_VALUE);
+ }
+
+ let composite_image_filename =
+ make_composite_image_filename(temporary_directory, next_temporary_image_id);
+ let (image, partition_files) =
+ make_composite_image(&disk.partitions, &composite_image_filename).map_err(|e| {
+ error!("Failed to make composite image with config {:?}: {:?}", disk, e);
+ StatusCode::UNKNOWN_ERROR
+ })?;
+
+ // Pass the file descriptors for the various partition files to crosvm when it
+ // is run.
+ indirect_files.extend(partition_files);
+
+ image
+ } else if let Some(image) = &disk.image {
+ clone_file(image)?
+ } else {
+ warn!("DiskImage {:?} didn't contain image or partitions.", disk);
+ return Err(StatusCode::BAD_VALUE);
+ };
+
+ Ok(DiskFile { image, writable: disk.writable })
+}
+
+/// Generates a unique filename to use for a composite disk image.
+fn make_composite_image_filename(
+ temporary_directory: &Path,
+ next_temporary_image_id: &mut u64,
+) -> PathBuf {
+ let id = *next_temporary_image_id;
+ *next_temporary_image_id += 1;
+ temporary_directory.join(format!("composite-{}.img", id))
+}
+
+/// Gets the calling SID of the current Binder thread.
+fn get_calling_sid() -> Result<String, StatusCode> {
+ ThreadState::with_calling_sid(|sid| {
+ if let Some(sid) = sid {
+ match sid.to_str() {
+ Ok(sid) => Ok(sid.to_owned()),
+ Err(e) => {
+ error!("SID was not valid UTF-8: {:?}", e);
+ Err(StatusCode::BAD_VALUE)
+ }
+ }
+ } else {
+ error!("Missing SID on startVm");
+ Err(StatusCode::UNKNOWN_ERROR)
+ }
+ })
+}
+
/// Check whether the caller of the current Binder method is allowed to call debug methods.
fn debug_access_allowed() -> bool {
let uid = ThreadState::get_calling_uid();
@@ -265,3 +374,13 @@
State { next_cid: FIRST_GUEST_CID, vms: vec![], debug_held_vms: vec![] }
}
}
+
+/// Converts an `&Option<T>` to an `Option<U>` where `T` implements `AsRef<U>`.
+fn as_asref<T: AsRef<U>, U>(option: &Option<T>) -> Option<&U> {
+ option.as_ref().map(|t| t.as_ref())
+}
+
+/// Converts a `&ParcelFileDescriptor` to a `File` by cloning the file.
+fn clone_file(file: &ParcelFileDescriptor) -> Result<File, StatusCode> {
+ file.as_ref().try_clone().map_err(|_| StatusCode::UNKNOWN_ERROR)
+}
diff --git a/virtualizationservice/src/composite.rs b/virtualizationservice/src/composite.rs
new file mode 100644
index 0000000..eb738a7
--- /dev/null
+++ b/virtualizationservice/src/composite.rs
@@ -0,0 +1,112 @@
+// Copyright 2021, 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.
+
+//! Functions for running `mk_cdisk`.
+
+mod config;
+
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::Partition::Partition as AidlPartition;
+use anyhow::{bail, Context, Error};
+use command_fds::{CommandFdExt, FdMapping};
+use config::{Config, Partition};
+use log::info;
+use std::fs::File;
+use std::os::unix::io::AsRawFd;
+use std::panic;
+use std::path::Path;
+use std::process::{Command, Stdio};
+use std::str;
+use std::thread;
+
+const MK_CDISK_PATH: &str = "/apex/com.android.virt/bin/mk_cdisk";
+
+/// Calls `mk_cdisk` to construct a composite disk image for the given list of partitions, and opens
+/// it ready to use. Returns the composite disk image file, and a list of FD mappings which must be
+/// applied to any process which wants to use it. This is necessary because the composite image
+/// contains paths of the form `/proc/self/fd/N` for the partition images.
+pub fn make_composite_image(
+ partitions: &[AidlPartition],
+ output_filename: &Path,
+) -> Result<(File, Vec<File>), Error> {
+ let (config_json, files) = make_config_json(partitions)?;
+ let fd_mappings: Vec<_> = files
+ .iter()
+ .map(|file| FdMapping { parent_fd: file.as_raw_fd(), child_fd: file.as_raw_fd() })
+ .collect();
+
+ let mut command = Command::new(MK_CDISK_PATH);
+ command
+ .arg("-") // Read config JSON from stdin.
+ .arg(&output_filename)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped());
+ command.fd_mappings(fd_mappings)?;
+ let mut child = command.spawn().context("Failed to spawn mk_cdisk")?;
+ let stdin = child.stdin.take().unwrap();
+
+ // Write config to stdin of mk_cdisk on a separate thread to avoid deadlock, as it may not read
+ // all of stdin before it blocks on writing to stdout.
+ let writer_thread = thread::spawn(move || config_json.write_json(&stdin));
+ info!("Running {:?}", command);
+ let output = child.wait_with_output()?;
+ match writer_thread.join() {
+ Ok(result) => result?,
+ Err(panic_payload) => panic::resume_unwind(panic_payload),
+ }
+
+ if !output.status.success() {
+ info!("mk_cdisk stdout: {}", str::from_utf8(&output.stdout)?);
+ info!("mk_cdisk stderr: {}", str::from_utf8(&output.stderr)?);
+ bail!("mk_cdisk exited with error {}", output.status);
+ }
+
+ let composite_image = File::open(&output_filename)
+ .with_context(|| format!("Failed to open composite image {:?}", output_filename))?;
+
+ Ok((composite_image, files))
+}
+
+/// Given the AIDL config containing a list of partitions, with a [`ParcelFileDescriptor`] for each
+/// partition, return the list of file descriptors which must be passed to the mk_cdisk child
+/// process and the JSON configuration for it.
+fn make_config_json(partitions: &[AidlPartition]) -> Result<(Config, Vec<File>), Error> {
+ // File descriptors to pass to child process.
+ let mut files = vec![];
+
+ let partitions = partitions
+ .iter()
+ .map(|partition| {
+ // TODO(b/187187765): This shouldn't be an Option.
+ let file = partition
+ .image
+ .as_ref()
+ .context("Invalid partition image file descriptor")?
+ .as_ref()
+ .try_clone()
+ .context("Failed to clone partition image file descriptor")?;
+ let fd = file.as_raw_fd();
+ files.push(file);
+
+ Ok(Partition {
+ writable: partition.writable,
+ label: partition.label.to_owned(),
+ path: format!("/proc/self/fd/{}", fd).into(),
+ })
+ })
+ .collect::<Result<_, Error>>()?;
+ let config_json = Config { partitions };
+
+ Ok((config_json, files))
+}
diff --git a/virtualizationservice/src/composite/config.rs b/virtualizationservice/src/composite/config.rs
new file mode 100644
index 0000000..1a915ba
--- /dev/null
+++ b/virtualizationservice/src/composite/config.rs
@@ -0,0 +1,44 @@
+// Copyright 2021, 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.
+
+//! JSON configuration for running `mk_cdisk`.
+
+use anyhow::{Context, Error};
+use serde::{Deserialize, Serialize};
+use std::io::Write;
+use std::path::PathBuf;
+
+#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
+pub struct Config {
+ /// The set of partitions to be assembled into a composite image.
+ pub partitions: Vec<Partition>,
+}
+
+/// A partition to be assembled into a composite image.
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct Partition {
+ /// A label for the partition.
+ pub label: String,
+ /// The filename of the partition image.
+ pub path: PathBuf,
+ /// Whether the partition should be writable.
+ #[serde(default)]
+ pub writable: bool,
+}
+
+impl Config {
+ pub fn write_json(&self, writer: impl Write) -> Result<(), Error> {
+ serde_json::to_writer(writer, self).context("Failed to write config JSON for mk_cdisk")
+ }
+}
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index 552941d..138236c 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -16,13 +16,13 @@
use crate::aidl::VirtualMachineCallbacks;
use crate::Cid;
-use android_system_virtualizationservice::aidl::android::system::virtualizationservice::VirtualMachineConfig::VirtualMachineConfig;
-use anyhow::{bail, Context, Error};
+use anyhow::{bail, Error};
use command_fds::{CommandFdExt, FdMapping};
use log::{debug, error, info};
use shared_child::SharedChild;
-use std::fs::File;
+use std::fs::{remove_dir_all, File};
use std::os::unix::io::AsRawFd;
+use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
@@ -30,6 +30,24 @@
const CROSVM_PATH: &str = "/apex/com.android.virt/bin/crosvm";
+/// Configuration for a VM to run with crosvm.
+#[derive(Debug)]
+pub struct CrosvmConfig<'a> {
+ pub cid: Cid,
+ pub bootloader: Option<&'a File>,
+ pub kernel: Option<&'a File>,
+ pub initrd: Option<&'a File>,
+ pub disks: Vec<DiskFile>,
+ pub params: Option<String>,
+}
+
+/// A disk image to pass to crosvm for a VM.
+#[derive(Debug)]
+pub struct DiskFile {
+ pub image: File,
+ pub writable: bool,
+}
+
/// Information about a particular instance of a VM which is running.
#[derive(Debug)]
pub struct VmInstance {
@@ -37,6 +55,8 @@
child: SharedChild,
/// The CID assigned to the VM for vsock communication.
pub cid: Cid,
+ /// Directory of temporary files used by the VM while it is running.
+ pub temporary_directory: PathBuf,
/// The UID of the process which requested the VM.
pub requester_uid: u32,
/// The SID of the process which requested the VM.
@@ -55,6 +75,7 @@
fn new(
child: SharedChild,
cid: Cid,
+ temporary_directory: PathBuf,
requester_uid: u32,
requester_sid: String,
requester_debug_pid: i32,
@@ -62,6 +83,7 @@
VmInstance {
child,
cid,
+ temporary_directory,
requester_uid,
requester_sid,
requester_debug_pid,
@@ -73,17 +95,19 @@
/// Start an instance of `crosvm` to manage a new VM. The `crosvm` instance will be killed when
/// the `VmInstance` is dropped.
pub fn start(
- config: &VirtualMachineConfig,
- cid: Cid,
+ config: &CrosvmConfig,
log_fd: Option<File>,
+ composite_disk_mappings: &[FdMapping],
+ temporary_directory: PathBuf,
requester_uid: u32,
requester_sid: String,
requester_debug_pid: i32,
) -> Result<Arc<VmInstance>, Error> {
- let child = run_vm(config, cid, log_fd)?;
+ let child = run_vm(config, log_fd, composite_disk_mappings)?;
let instance = Arc::new(VmInstance::new(
child,
- cid,
+ config.cid,
+ temporary_directory,
requester_uid,
requester_sid,
requester_debug_pid,
@@ -106,6 +130,11 @@
}
self.running.store(false, Ordering::Release);
self.callbacks.callback_on_died(self.cid);
+
+ // Delete temporary files.
+ if let Err(e) = remove_dir_all(&self.temporary_directory) {
+ error!("Error removing temporary directory {:?}: {:?}", self.temporary_directory, e);
+ }
}
/// Return whether `crosvm` is still running the VM.
@@ -124,15 +153,15 @@
/// Start an instance of `crosvm` to manage a new VM.
fn run_vm(
- config: &VirtualMachineConfig,
- cid: Cid,
+ config: &CrosvmConfig,
log_fd: Option<File>,
+ composite_disk_mappings: &[FdMapping],
) -> Result<SharedChild, Error> {
validate_config(config)?;
let mut command = Command::new(CROSVM_PATH);
// TODO(qwandor): Remove --disable-sandbox.
- command.arg("run").arg("--disable-sandbox").arg("--cid").arg(cid.to_string());
+ command.arg("run").arg("--disable-sandbox").arg("--cid").arg(config.cid.to_string());
if let Some(log_fd) = log_fd {
command.stdout(log_fd);
@@ -142,14 +171,14 @@
}
// Keep track of what file descriptors should be mapped to the crosvm process.
- let mut fd_mappings = vec![];
+ let mut fd_mappings = composite_disk_mappings.to_vec();
if let Some(bootloader) = &config.bootloader {
- command.arg("--bios").arg(add_fd_mapping(&mut fd_mappings, bootloader.as_ref()));
+ command.arg("--bios").arg(add_fd_mapping(&mut fd_mappings, bootloader));
}
if let Some(initrd) = &config.initrd {
- command.arg("--initrd").arg(add_fd_mapping(&mut fd_mappings, initrd.as_ref()));
+ command.arg("--initrd").arg(add_fd_mapping(&mut fd_mappings, initrd));
}
if let Some(params) = &config.params {
@@ -157,15 +186,13 @@
}
for disk in &config.disks {
- command.arg(if disk.writable { "--rwdisk" } else { "--disk" }).arg(add_fd_mapping(
- &mut fd_mappings,
- // TODO(b/187187765): This shouldn't be an Option.
- disk.image.as_ref().context("Invalid disk image file descriptor")?.as_ref(),
- ));
+ command
+ .arg(if disk.writable { "--rwdisk" } else { "--disk" })
+ .arg(add_fd_mapping(&mut fd_mappings, &disk.image));
}
if let Some(kernel) = &config.kernel {
- command.arg(add_fd_mapping(&mut fd_mappings, kernel.as_ref()));
+ command.arg(add_fd_mapping(&mut fd_mappings, kernel));
}
debug!("Setting mappings {:?}", fd_mappings);
@@ -177,7 +204,7 @@
}
/// Ensure that the configuration has a valid combination of fields set, or return an error if not.
-fn validate_config(config: &VirtualMachineConfig) -> Result<(), Error> {
+fn validate_config(config: &CrosvmConfig) -> Result<(), Error> {
if config.bootloader.is_none() && config.kernel.is_none() {
bail!("VM must have either a bootloader or a kernel image.");
}
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 5453146..cf0be38 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -15,6 +15,7 @@
//! Android VirtualizationService
mod aidl;
+mod composite;
mod crosvm;
use crate::aidl::{VirtualizationService, BINDER_SERVICE_IDENTIFIER};