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