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     /// Whether the VM should be debuggable.
62     pub debug_mode: bool,
63     /// CPU topology of the VM. Defaults to 1 vCPU.
64     pub cpu_topology: VmCpuTopology,
65     /// If present, overrides the amount of RAM to give the VM
66     pub memory_mib: Option<i32>,
67     /// Whether the VM prefers staged APEXes or activated ones (false; default)
68     pub prefer_staged: bool,
69 }
70 
71 impl ComposClient {
72     /// 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>73     pub fn start(
74         service: &dyn IVirtualizationService,
75         instance_id: [u8; 64],
76         instance_image: File,
77         idsig: &Path,
78         idsig_manifest_apk: &Path,
79         idsig_manifest_ext_apk: &Path,
80         parameters: &VmParameters,
81     ) -> Result<Self> {
82         let have_protected_vm =
83             hypervisorproperties::hypervisor_protected_vm_supported()?.unwrap_or(false);
84         if !have_protected_vm {
85             bail!("Protected VM not supported, unable to start VM");
86         }
87 
88         let instance_fd = ParcelFileDescriptor::new(instance_image);
89 
90         let apex_dir = Path::new(COMPOS_APEX_ROOT);
91 
92         let config_apk = locate_config_apk(apex_dir)?;
93         let apk_fd = File::open(config_apk).context("Failed to open config APK file")?;
94         let apk_fd = ParcelFileDescriptor::new(apk_fd);
95         let idsig_fd = prepare_idsig(service, &apk_fd, idsig)?;
96 
97         let manifest_apk_fd = File::open(BUILD_MANIFEST_APK_PATH)
98             .context("Failed to open build manifest APK file")?;
99         let manifest_apk_fd = ParcelFileDescriptor::new(manifest_apk_fd);
100         let idsig_manifest_apk_fd = prepare_idsig(service, &manifest_apk_fd, idsig_manifest_apk)?;
101 
102         // Prepare a few things based on whether /system_ext exists, including:
103         // 1. generate the additional idsig FD for the APK from /system_ext, then pass to VS
104         // 2. select the correct VM config json
105         let (extra_idsigs, has_system_ext) =
106             if let Ok(manifest_ext_apk_fd) = File::open(BUILD_MANIFEST_SYSTEM_EXT_APK_PATH) {
107                 // Optional idsig in /system_ext is found, so prepare additionally.
108                 let manifest_ext_apk_fd = ParcelFileDescriptor::new(manifest_ext_apk_fd);
109                 let idsig_manifest_ext_apk_fd =
110                     prepare_idsig(service, &manifest_ext_apk_fd, idsig_manifest_ext_apk)?;
111 
112                 (vec![idsig_manifest_apk_fd, idsig_manifest_ext_apk_fd], true)
113             } else {
114                 (vec![idsig_manifest_apk_fd], false)
115             };
116         let config_path = get_vm_config_path(has_system_ext, parameters.prefer_staged);
117 
118         let debug_level = if parameters.debug_mode { DebugLevel::FULL } else { DebugLevel::NONE };
119 
120         let cpu_topology = match parameters.cpu_topology {
121             VmCpuTopology::OneCpu => CpuTopology::ONE_CPU,
122             VmCpuTopology::MatchHost => CpuTopology::MATCH_HOST,
123         };
124 
125         // The CompOS VM doesn't need to be updatable (by design it should run exactly twice,
126         // with the same APKs and APEXes each time). And having it so causes some interesting
127         // circular dependencies when run at boot time by odsign: b/331417880.
128         let custom_config = Some(CustomConfig { wantUpdatable: false, ..Default::default() });
129 
130         let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
131             name: parameters.name.clone(),
132             apk: Some(apk_fd),
133             idsig: Some(idsig_fd),
134             instanceId: instance_id,
135             instanceImage: Some(instance_fd),
136             payload: Payload::ConfigPath(config_path),
137             debugLevel: debug_level,
138             extraIdsigs: extra_idsigs,
139             protectedVm: true,
140             memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default
141             cpuTopology: cpu_topology,
142             customConfig: custom_config,
143             ..Default::default()
144         });
145 
146         // Let logs go to logcat.
147         let (console_fd, log_fd) = (None, None);
148         let callback = Box::new(Callback {});
149         let instance = VmInstance::create(
150             service,
151             &config,
152             console_fd,
153             /* console_in_fd */ None,
154             log_fd,
155             Some(callback),
156         )
157         .context("Failed to create VM")?;
158 
159         instance.start()?;
160 
161         let ready = instance.wait_until_ready(TIMEOUTS.vm_max_time_to_ready);
162         if ready == Err(VmWaitError::Finished) && debug_level != DebugLevel::NONE {
163             // The payload has (unexpectedly) finished, but the VM is still running. Give it
164             // some time to shutdown to maximize our chances of getting useful logs.
165             if let Some(death_reason) =
166                 instance.wait_for_death_with_timeout(TIMEOUTS.vm_max_time_to_exit)
167             {
168                 bail!("VM died during startup - reason {:?}", death_reason);
169             }
170         }
171         ready?;
172 
173         Ok(Self(instance))
174     }
175 
176     /// Create and return an RPC Binder connection to the Comp OS service in the VM.
connect_service(&self) -> Result<Strong<dyn ICompOsService>>177     pub fn connect_service(&self) -> Result<Strong<dyn ICompOsService>> {
178         self.0.connect_service(COMPOS_VSOCK_PORT).context("Connecting to CompOS service")
179     }
180 
181     /// Shut down the VM cleanly, by sending a quit request to the service, giving time for any
182     /// relevant logs to be written.
shutdown(self, service: Strong<dyn ICompOsService>)183     pub fn shutdown(self, service: Strong<dyn ICompOsService>) {
184         info!("Requesting CompOS VM to shutdown");
185         let _ignored = service.quit(); // If this fails, the VM is probably dying anyway
186         self.wait_for_shutdown();
187     }
188 
189     /// Wait for the instance to shut down. If it fails to shutdown within a reasonable time the
190     /// instance is dropped, which forcibly terminates it.
191     /// This should only be called when the instance has been requested to quit, or we believe that
192     /// it is already in the process of exiting due to some failure.
wait_for_shutdown(self)193     fn wait_for_shutdown(self) {
194         let death_reason = self.0.wait_for_death_with_timeout(TIMEOUTS.vm_max_time_to_exit);
195         match death_reason {
196             Some(DeathReason::Shutdown) => info!("VM has exited normally"),
197             Some(reason) => warn!("VM died with reason {:?}", reason),
198             None => warn!("VM failed to exit, dropping"),
199         }
200     }
201 }
202 
locate_config_apk(apex_dir: &Path) -> Result<PathBuf>203 fn locate_config_apk(apex_dir: &Path) -> Result<PathBuf> {
204     // Our config APK will be in a directory under app, but the name of the directory is at the
205     // discretion of the build system. So just look in each sub-directory until we find it.
206     // (In practice there will be exactly one directory, so this shouldn't take long.)
207     let app_glob = apex_dir.join("app").join("**").join("CompOSPayloadApp*.apk");
208     let mut entries: Vec<PathBuf> =
209         glob(app_glob.to_str().ok_or_else(|| anyhow!("Invalid path: {}", app_glob.display()))?)
210             .context("failed to glob")?
211             .filter_map(|e| e.ok())
212             .collect();
213     if entries.len() > 1 {
214         bail!("Found more than one apk matching {}", app_glob.display());
215     }
216     match entries.pop() {
217         Some(path) => Ok(path),
218         None => Err(anyhow!("No apks match {}", app_glob.display())),
219     }
220 }
221 
prepare_idsig( service: &dyn IVirtualizationService, apk_fd: &ParcelFileDescriptor, idsig_path: &Path, ) -> Result<ParcelFileDescriptor>222 fn prepare_idsig(
223     service: &dyn IVirtualizationService,
224     apk_fd: &ParcelFileDescriptor,
225     idsig_path: &Path,
226 ) -> Result<ParcelFileDescriptor> {
227     if !idsig_path.exists() {
228         // Prepare idsig file via VirtualizationService
229         let idsig_file = File::create(idsig_path).context("Failed to create idsig file")?;
230         let idsig_fd = ParcelFileDescriptor::new(idsig_file);
231         service
232             .createOrUpdateIdsigFile(apk_fd, &idsig_fd)
233             .context("Failed to update idsig file")?;
234     }
235 
236     // Open idsig as read-only
237     let idsig_file = File::open(idsig_path).context("Failed to open idsig file")?;
238     let idsig_fd = ParcelFileDescriptor::new(idsig_file);
239     Ok(idsig_fd)
240 }
241 
242 struct Callback {}
243 impl vmclient::VmCallback for Callback {
on_payload_started(&self, cid: i32)244     fn on_payload_started(&self, cid: i32) {
245         log::info!("VM payload started, cid = {}", cid);
246     }
247 
on_payload_ready(&self, cid: i32)248     fn on_payload_ready(&self, cid: i32) {
249         log::info!("VM payload ready, cid = {}", cid);
250     }
251 
on_payload_finished(&self, cid: i32, exit_code: i32)252     fn on_payload_finished(&self, cid: i32, exit_code: i32) {
253         log::warn!("VM payload finished, cid = {}, exit code = {}", cid, exit_code);
254     }
255 
on_error(&self, cid: i32, error_code: ErrorCode, message: &str)256     fn on_error(&self, cid: i32, error_code: ErrorCode, message: &str) {
257         log::warn!("VM error, cid = {}, error code = {:?}, message = {}", cid, error_code, message);
258     }
259 
on_died(&self, cid: i32, death_reason: DeathReason)260     fn on_died(&self, cid: i32, death_reason: DeathReason) {
261         log::warn!("VM died, cid = {}, reason = {:?}", cid, death_reason);
262     }
263 }
264