1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 //! Support for starting CompOS in a VM and connecting to the service
18 
19 use crate::timeouts::TIMEOUTS;
20 use crate::{
21     get_vm_config_path, BUILD_MANIFEST_APK_PATH, BUILD_MANIFEST_SYSTEM_EXT_APK_PATH,
22     COMPOS_APEX_ROOT, COMPOS_VSOCK_PORT,
23 };
24 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
25     CpuTopology::CpuTopology,
26     IVirtualizationService::IVirtualizationService,
27     VirtualMachineAppConfig::{
28         CustomConfig::CustomConfig, DebugLevel::DebugLevel, Payload::Payload,
29         VirtualMachineAppConfig,
30     },
31     VirtualMachineConfig::VirtualMachineConfig,
32 };
33 use anyhow::{anyhow, bail, Context, Result};
34 use binder::{ParcelFileDescriptor, Strong};
35 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
36 use glob::glob;
37 use log::{info, warn};
38 use platformproperties::hypervisorproperties;
39 use std::fs::File;
40 use std::path::{Path, PathBuf};
41 use vmclient::{DeathReason, ErrorCode, VmInstance, VmWaitError};
42 
43 /// This owns an instance of the CompOS VM.
44 pub struct ComposClient(VmInstance);
45 
46 /// CPU topology configuration for a virtual machine.
47 #[derive(Default, Debug, Clone)]
48 pub enum VmCpuTopology {
49     /// Run VM with 1 vCPU only.
50     #[default]
51     OneCpu,
52     /// Run VM vCPU topology matching that of the host.
53     MatchHost,
54 }
55 
56 /// Parameters to be used when creating a virtual machine instance.
57 #[derive(Default, Debug, Clone)]
58 pub struct VmParameters {
59     /// The name of VM for identifying.
60     pub name: String,
61     /// The OS of VM.
62     pub os: String,
63     /// Whether the VM should be debuggable.
64     pub debug_mode: bool,
65     /// CPU topology of the VM. Defaults to 1 vCPU.
66     pub cpu_topology: VmCpuTopology,
67     /// If present, overrides the amount of RAM to give the VM
68     pub memory_mib: Option<i32>,
69     /// Whether the VM prefers staged APEXes or activated ones (false; default)
70     pub prefer_staged: bool,
71 }
72 
73 impl ComposClient {
74     /// Start a new CompOS VM instance using the specified instance image file and parameters.
start( service: &dyn IVirtualizationService, instance_id: [u8; 64], instance_image: File, idsig: &Path, idsig_manifest_apk: &Path, idsig_manifest_ext_apk: &Path, parameters: &VmParameters, ) -> Result<Self>75     pub fn start(
76         service: &dyn IVirtualizationService,
77         instance_id: [u8; 64],
78         instance_image: File,
79         idsig: &Path,
80         idsig_manifest_apk: &Path,
81         idsig_manifest_ext_apk: &Path,
82         parameters: &VmParameters,
83     ) -> Result<Self> {
84         let have_protected_vm =
85             hypervisorproperties::hypervisor_protected_vm_supported()?.unwrap_or(false);
86         if !have_protected_vm {
87             bail!("Protected VM not supported, unable to start VM");
88         }
89 
90         let instance_fd = ParcelFileDescriptor::new(instance_image);
91 
92         let apex_dir = Path::new(COMPOS_APEX_ROOT);
93 
94         let config_apk = locate_config_apk(apex_dir)?;
95         let apk_fd = File::open(config_apk).context("Failed to open config APK file")?;
96         let apk_fd = ParcelFileDescriptor::new(apk_fd);
97         let idsig_fd = prepare_idsig(service, &apk_fd, idsig)?;
98 
99         let manifest_apk_fd = File::open(BUILD_MANIFEST_APK_PATH)
100             .context("Failed to open build manifest APK file")?;
101         let manifest_apk_fd = ParcelFileDescriptor::new(manifest_apk_fd);
102         let idsig_manifest_apk_fd = prepare_idsig(service, &manifest_apk_fd, idsig_manifest_apk)?;
103 
104         // Prepare a few things based on whether /system_ext exists, including:
105         // 1. generate the additional idsig FD for the APK from /system_ext, then pass to VS
106         // 2. select the correct VM config json
107         let (extra_idsigs, has_system_ext) =
108             if let Ok(manifest_ext_apk_fd) = File::open(BUILD_MANIFEST_SYSTEM_EXT_APK_PATH) {
109                 // Optional idsig in /system_ext is found, so prepare additionally.
110                 let manifest_ext_apk_fd = ParcelFileDescriptor::new(manifest_ext_apk_fd);
111                 let idsig_manifest_ext_apk_fd =
112                     prepare_idsig(service, &manifest_ext_apk_fd, idsig_manifest_ext_apk)?;
113 
114                 (vec![idsig_manifest_apk_fd, idsig_manifest_ext_apk_fd], true)
115             } else {
116                 (vec![idsig_manifest_apk_fd], false)
117             };
118         let config_path = get_vm_config_path(has_system_ext, parameters.prefer_staged);
119 
120         let debug_level = if parameters.debug_mode { DebugLevel::FULL } else { DebugLevel::NONE };
121 
122         let cpu_topology = match parameters.cpu_topology {
123             VmCpuTopology::OneCpu => CpuTopology::ONE_CPU,
124             VmCpuTopology::MatchHost => CpuTopology::MATCH_HOST,
125         };
126 
127         // The CompOS VM doesn't need to be updatable (by design it should run exactly twice,
128         // with the same APKs and APEXes each time). And having it so causes some interesting
129         // circular dependencies when run at boot time by odsign: b/331417880.
130         let custom_config = Some(CustomConfig { wantUpdatable: false, ..Default::default() });
131 
132         let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
133             name: parameters.name.clone(),
134             osName: parameters.os.clone(),
135             apk: Some(apk_fd),
136             idsig: Some(idsig_fd),
137             instanceId: instance_id,
138             instanceImage: Some(instance_fd),
139             payload: Payload::ConfigPath(config_path),
140             debugLevel: debug_level,
141             extraIdsigs: extra_idsigs,
142             protectedVm: true,
143             memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default
144             cpuTopology: cpu_topology,
145             customConfig: custom_config,
146             ..Default::default()
147         });
148 
149         // Let logs go to logcat.
150         let (console_fd, log_fd) = (None, None);
151         let callback = Box::new(Callback {});
152         let instance = VmInstance::create(
153             service,
154             &config,
155             console_fd,
156             /* console_in_fd */ None,
157             log_fd,
158             /* dump_dt */ None,
159             Some(callback),
160         )
161         .context("Failed to create VM")?;
162 
163         instance.start()?;
164 
165         let ready = instance.wait_until_ready(TIMEOUTS.vm_max_time_to_ready);
166         if ready == Err(VmWaitError::Finished) && debug_level != DebugLevel::NONE {
167             // The payload has (unexpectedly) finished, but the VM is still running. Give it
168             // some time to shutdown to maximize our chances of getting useful logs.
169             if let Some(death_reason) =
170                 instance.wait_for_death_with_timeout(TIMEOUTS.vm_max_time_to_exit)
171             {
172                 bail!("VM died during startup - reason {:?}", death_reason);
173             }
174         }
175         ready?;
176 
177         Ok(Self(instance))
178     }
179 
180     /// Create and return an RPC Binder connection to the Comp OS service in the VM.
connect_service(&self) -> Result<Strong<dyn ICompOsService>>181     pub fn connect_service(&self) -> Result<Strong<dyn ICompOsService>> {
182         self.0.connect_service(COMPOS_VSOCK_PORT).context("Connecting to CompOS service")
183     }
184 
185     /// Shut down the VM cleanly, by sending a quit request to the service, giving time for any
186     /// relevant logs to be written.
shutdown(self, service: Strong<dyn ICompOsService>)187     pub fn shutdown(self, service: Strong<dyn ICompOsService>) {
188         info!("Requesting CompOS VM to shutdown");
189         let _ignored = service.quit(); // If this fails, the VM is probably dying anyway
190         self.wait_for_shutdown();
191     }
192 
193     /// Wait for the instance to shut down. If it fails to shutdown within a reasonable time the
194     /// instance is dropped, which forcibly terminates it.
195     /// This should only be called when the instance has been requested to quit, or we believe that
196     /// it is already in the process of exiting due to some failure.
wait_for_shutdown(self)197     fn wait_for_shutdown(self) {
198         let death_reason = self.0.wait_for_death_with_timeout(TIMEOUTS.vm_max_time_to_exit);
199         match death_reason {
200             Some(DeathReason::Shutdown) => info!("VM has exited normally"),
201             Some(reason) => warn!("VM died with reason {:?}", reason),
202             None => warn!("VM failed to exit, dropping"),
203         }
204     }
205 }
206 
locate_config_apk(apex_dir: &Path) -> Result<PathBuf>207 fn locate_config_apk(apex_dir: &Path) -> Result<PathBuf> {
208     // Our config APK will be in a directory under app, but the name of the directory is at the
209     // discretion of the build system. So just look in each sub-directory until we find it.
210     // (In practice there will be exactly one directory, so this shouldn't take long.)
211     let app_glob = apex_dir.join("app").join("**").join("CompOSPayloadApp*.apk");
212     let mut entries: Vec<PathBuf> =
213         glob(app_glob.to_str().ok_or_else(|| anyhow!("Invalid path: {}", app_glob.display()))?)
214             .context("failed to glob")?
215             .filter_map(|e| e.ok())
216             .collect();
217     if entries.len() > 1 {
218         bail!("Found more than one apk matching {}", app_glob.display());
219     }
220     match entries.pop() {
221         Some(path) => Ok(path),
222         None => Err(anyhow!("No apks match {}", app_glob.display())),
223     }
224 }
225 
prepare_idsig( service: &dyn IVirtualizationService, apk_fd: &ParcelFileDescriptor, idsig_path: &Path, ) -> Result<ParcelFileDescriptor>226 fn prepare_idsig(
227     service: &dyn IVirtualizationService,
228     apk_fd: &ParcelFileDescriptor,
229     idsig_path: &Path,
230 ) -> Result<ParcelFileDescriptor> {
231     if !idsig_path.exists() {
232         // Prepare idsig file via VirtualizationService
233         let idsig_file = File::create(idsig_path).context("Failed to create idsig file")?;
234         let idsig_fd = ParcelFileDescriptor::new(idsig_file);
235         service
236             .createOrUpdateIdsigFile(apk_fd, &idsig_fd)
237             .context("Failed to update idsig file")?;
238     }
239 
240     // Open idsig as read-only
241     let idsig_file = File::open(idsig_path).context("Failed to open idsig file")?;
242     let idsig_fd = ParcelFileDescriptor::new(idsig_file);
243     Ok(idsig_fd)
244 }
245 
246 struct Callback {}
247 impl vmclient::VmCallback for Callback {
on_payload_started(&self, cid: i32)248     fn on_payload_started(&self, cid: i32) {
249         log::info!("VM payload started, cid = {}", cid);
250     }
251 
on_payload_ready(&self, cid: i32)252     fn on_payload_ready(&self, cid: i32) {
253         log::info!("VM payload ready, cid = {}", cid);
254     }
255 
on_payload_finished(&self, cid: i32, exit_code: i32)256     fn on_payload_finished(&self, cid: i32, exit_code: i32) {
257         log::warn!("VM payload finished, cid = {}, exit code = {}", cid, exit_code);
258     }
259 
on_error(&self, cid: i32, error_code: ErrorCode, message: &str)260     fn on_error(&self, cid: i32, error_code: ErrorCode, message: &str) {
261         log::warn!("VM error, cid = {}, error code = {:?}, message = {}", cid, error_code, message);
262     }
263 
on_died(&self, cid: i32, death_reason: DeathReason)264     fn on_died(&self, cid: i32, death_reason: DeathReason) {
265         log::warn!("VM died, cid = {}, reason = {:?}", cid, death_reason);
266     }
267 }
268