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