| /* |
| * Copyright (C) 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. |
| */ |
| |
| //! Support for starting CompOS in a VM and connecting to the service |
| |
| use crate::timeouts::TIMEOUTS; |
| use crate::{ |
| get_vm_config_path, BUILD_MANIFEST_APK_PATH, BUILD_MANIFEST_SYSTEM_EXT_APK_PATH, |
| COMPOS_APEX_ROOT, COMPOS_DATA_ROOT, COMPOS_VSOCK_PORT, |
| }; |
| use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{ |
| CpuTopology::CpuTopology, |
| IVirtualizationService::IVirtualizationService, |
| VirtualMachineAppConfig::{DebugLevel::DebugLevel, Payload::Payload, VirtualMachineAppConfig}, |
| VirtualMachineConfig::VirtualMachineConfig, |
| }; |
| use anyhow::{anyhow, bail, Context, Result}; |
| use binder::{ParcelFileDescriptor, Strong}; |
| use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService; |
| use glob::glob; |
| use log::{info, warn}; |
| use rustutils::system_properties; |
| use std::fs::File; |
| use std::path::{Path, PathBuf}; |
| use vmclient::{DeathReason, ErrorCode, VmInstance, VmWaitError}; |
| |
| /// This owns an instance of the CompOS VM. |
| pub struct ComposClient(VmInstance); |
| |
| /// CPU topology configuration for a virtual machine. |
| #[derive(Default, Debug, Clone)] |
| pub enum VmCpuTopology { |
| /// Run VM with 1 vCPU only. |
| #[default] |
| OneCpu, |
| /// Run VM vCPU topology matching that of the host. |
| MatchHost, |
| } |
| |
| /// Parameters to be used when creating a virtual machine instance. |
| #[derive(Default, Debug, Clone)] |
| pub struct VmParameters { |
| /// Whether the VM should be debuggable. |
| pub debug_mode: bool, |
| /// CPU topology of the VM. Defaults to 1 vCPU. |
| pub cpu_topology: VmCpuTopology, |
| /// List of task profiles to apply to the VM |
| pub task_profiles: Vec<String>, |
| /// If present, overrides the amount of RAM to give the VM |
| pub memory_mib: Option<i32>, |
| /// Whether the VM prefers staged APEXes or activated ones (false; default) |
| pub prefer_staged: bool, |
| } |
| |
| impl ComposClient { |
| /// Start a new CompOS VM instance using the specified instance image file and parameters. |
| pub fn start( |
| service: &dyn IVirtualizationService, |
| instance_image: File, |
| idsig: &Path, |
| idsig_manifest_apk: &Path, |
| idsig_manifest_ext_apk: &Path, |
| parameters: &VmParameters, |
| ) -> Result<Self> { |
| let protected_vm = want_protected_vm()?; |
| |
| let instance_fd = ParcelFileDescriptor::new(instance_image); |
| |
| let apex_dir = Path::new(COMPOS_APEX_ROOT); |
| let data_dir = Path::new(COMPOS_DATA_ROOT); |
| |
| let config_apk = locate_config_apk(apex_dir)?; |
| let apk_fd = File::open(config_apk).context("Failed to open config APK file")?; |
| let apk_fd = ParcelFileDescriptor::new(apk_fd); |
| let idsig_fd = prepare_idsig(service, &apk_fd, idsig)?; |
| |
| let manifest_apk_fd = File::open(BUILD_MANIFEST_APK_PATH) |
| .context("Failed to open build manifest APK file")?; |
| let manifest_apk_fd = ParcelFileDescriptor::new(manifest_apk_fd); |
| let idsig_manifest_apk_fd = prepare_idsig(service, &manifest_apk_fd, idsig_manifest_apk)?; |
| |
| // Prepare a few things based on whether /system_ext exists, including: |
| // 1. generate the additional idsig FD for the APK from /system_ext, then pass to VS |
| // 2. select the correct VM config json |
| let (extra_idsigs, has_system_ext) = |
| if let Ok(manifest_ext_apk_fd) = File::open(BUILD_MANIFEST_SYSTEM_EXT_APK_PATH) { |
| // Optional idsig in /system_ext is found, so prepare additionally. |
| let manifest_ext_apk_fd = ParcelFileDescriptor::new(manifest_ext_apk_fd); |
| let idsig_manifest_ext_apk_fd = |
| prepare_idsig(service, &manifest_ext_apk_fd, idsig_manifest_ext_apk)?; |
| |
| (vec![idsig_manifest_apk_fd, idsig_manifest_ext_apk_fd], true) |
| } else { |
| (vec![idsig_manifest_apk_fd], false) |
| }; |
| let config_path = get_vm_config_path(has_system_ext, parameters.prefer_staged); |
| |
| let debug_level = if parameters.debug_mode { DebugLevel::FULL } else { DebugLevel::NONE }; |
| |
| let (console_fd, log_fd) = if debug_level == DebugLevel::NONE { |
| (None, None) |
| } else { |
| // Console output and the system log output from the VM are redirected to file. |
| let console_fd = File::create(data_dir.join("vm_console.log")) |
| .context("Failed to create console log file")?; |
| let log_fd = File::create(data_dir.join("vm.log")) |
| .context("Failed to create system log file")?; |
| info!("Running in debug level {:?}", debug_level); |
| (Some(console_fd), Some(log_fd)) |
| }; |
| |
| let cpu_topology = match parameters.cpu_topology { |
| VmCpuTopology::OneCpu => CpuTopology::ONE_CPU, |
| VmCpuTopology::MatchHost => CpuTopology::MATCH_HOST, |
| }; |
| |
| let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig { |
| name: String::from("Compos"), |
| apk: Some(apk_fd), |
| idsig: Some(idsig_fd), |
| instanceImage: Some(instance_fd), |
| encryptedStorageImage: None, |
| payload: Payload::ConfigPath(config_path), |
| debugLevel: debug_level, |
| extraIdsigs: extra_idsigs, |
| protectedVm: protected_vm, |
| memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default |
| cpuTopology: cpu_topology, |
| taskProfiles: parameters.task_profiles.clone(), |
| gdbPort: 0, // Don't start gdb-server |
| }); |
| |
| let callback = Box::new(Callback {}); |
| let instance = VmInstance::create(service, &config, console_fd, log_fd, Some(callback)) |
| .context("Failed to create VM")?; |
| |
| instance.start()?; |
| |
| let ready = instance.wait_until_ready(TIMEOUTS.vm_max_time_to_ready); |
| if ready == Err(VmWaitError::Finished) && debug_level != DebugLevel::NONE { |
| // The payload has (unexpectedly) finished, but the VM is still running. Give it |
| // some time to shutdown to maximize our chances of getting useful logs. |
| if let Some(death_reason) = |
| instance.wait_for_death_with_timeout(TIMEOUTS.vm_max_time_to_exit) |
| { |
| bail!("VM died during startup - reason {:?}", death_reason); |
| } |
| } |
| ready?; |
| |
| Ok(Self(instance)) |
| } |
| |
| /// Create and return an RPC Binder connection to the Comp OS service in the VM. |
| pub fn connect_service(&self) -> Result<Strong<dyn ICompOsService>> { |
| self.0.connect_service(COMPOS_VSOCK_PORT).context("Connecting to CompOS service") |
| } |
| |
| /// Shut down the VM cleanly, by sending a quit request to the service, giving time for any |
| /// relevant logs to be written. |
| pub fn shutdown(self, service: Strong<dyn ICompOsService>) { |
| info!("Requesting CompOS VM to shutdown"); |
| let _ = service.quit(); // If this fails, the VM is probably dying anyway |
| self.wait_for_shutdown(); |
| } |
| |
| /// Wait for the instance to shut down. If it fails to shutdown within a reasonable time the |
| /// instance is dropped, which forcibly terminates it. |
| /// This should only be called when the instance has been requested to quit, or we believe that |
| /// it is already in the process of exiting due to some failure. |
| fn wait_for_shutdown(self) { |
| let death_reason = self.0.wait_for_death_with_timeout(TIMEOUTS.vm_max_time_to_exit); |
| match death_reason { |
| Some(DeathReason::Shutdown) => info!("VM has exited normally"), |
| Some(reason) => warn!("VM died with reason {:?}", reason), |
| None => warn!("VM failed to exit, dropping"), |
| } |
| } |
| } |
| |
| fn locate_config_apk(apex_dir: &Path) -> Result<PathBuf> { |
| // Our config APK will be in a directory under app, but the name of the directory is at the |
| // discretion of the build system. So just look in each sub-directory until we find it. |
| // (In practice there will be exactly one directory, so this shouldn't take long.) |
| let app_glob = apex_dir.join("app").join("**").join("CompOSPayloadApp*.apk"); |
| let mut entries: Vec<PathBuf> = |
| glob(app_glob.to_str().ok_or_else(|| anyhow!("Invalid path: {}", app_glob.display()))?) |
| .context("failed to glob")? |
| .filter_map(|e| e.ok()) |
| .collect(); |
| if entries.len() > 1 { |
| bail!("Found more than one apk matching {}", app_glob.display()); |
| } |
| match entries.pop() { |
| Some(path) => Ok(path), |
| None => Err(anyhow!("No apks match {}", app_glob.display())), |
| } |
| } |
| |
| fn prepare_idsig( |
| service: &dyn IVirtualizationService, |
| apk_fd: &ParcelFileDescriptor, |
| idsig_path: &Path, |
| ) -> Result<ParcelFileDescriptor> { |
| if !idsig_path.exists() { |
| // Prepare idsig file via VirtualizationService |
| let idsig_file = File::create(idsig_path).context("Failed to create idsig file")?; |
| let idsig_fd = ParcelFileDescriptor::new(idsig_file); |
| service |
| .createOrUpdateIdsigFile(apk_fd, &idsig_fd) |
| .context("Failed to update idsig file")?; |
| } |
| |
| // Open idsig as read-only |
| let idsig_file = File::open(idsig_path).context("Failed to open idsig file")?; |
| let idsig_fd = ParcelFileDescriptor::new(idsig_file); |
| Ok(idsig_fd) |
| } |
| |
| fn want_protected_vm() -> Result<bool> { |
| let have_protected_vm = |
| system_properties::read_bool("ro.boot.hypervisor.protected_vm.supported", false)?; |
| if have_protected_vm { |
| info!("Starting protected VM"); |
| return Ok(true); |
| } |
| |
| let is_debug_build = system_properties::read("ro.debuggable")?.as_deref().unwrap_or("0") == "1"; |
| if !is_debug_build { |
| bail!("Protected VM not supported, unable to start VM"); |
| } |
| |
| let have_non_protected_vm = |
| system_properties::read_bool("ro.boot.hypervisor.vm.supported", false)?; |
| if have_non_protected_vm { |
| warn!("Protected VM not supported, falling back to non-protected on debuggable build"); |
| return Ok(false); |
| } |
| |
| bail!("No VM support available") |
| } |
| |
| struct Callback {} |
| impl vmclient::VmCallback for Callback { |
| fn on_payload_started(&self, cid: i32) { |
| log::info!("VM payload started, cid = {}", cid); |
| } |
| |
| fn on_payload_ready(&self, cid: i32) { |
| log::info!("VM payload ready, cid = {}", cid); |
| } |
| |
| fn on_payload_finished(&self, cid: i32, exit_code: i32) { |
| log::warn!("VM payload finished, cid = {}, exit code = {}", cid, exit_code); |
| } |
| |
| fn on_error(&self, cid: i32, error_code: ErrorCode, message: &str) { |
| log::warn!("VM error, cid = {}, error code = {:?}, message = {}", cid, error_code, message); |
| } |
| |
| fn on_died(&self, cid: i32, death_reason: DeathReason) { |
| log::warn!("VM died, cid = {}, reason = {:?}", cid, death_reason); |
| } |
| } |